From c4df1df85627bb0b817fe0067e4ac824bc4be9db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:08:08 +0000 Subject: [PATCH 01/24] Initial plan From 2722779d90d349af77e764235d4908682d869837 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:13:23 +0000 Subject: [PATCH 02/24] =?UTF-8?q?Add=20all=20missing=20methods=20and=20fie?= =?UTF-8?q?lds=20to=20=E4=BB=99=E5=89=9198DX9.cs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 782 +++++++++++++++++- 1 file changed, 772 insertions(+), 10 deletions(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 434d9ea..996b7bc 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -68,6 +68,12 @@ public override bool IsShowC() private bool IsShowSpeed = false; private bool HasAlertMutiPal = false; + private float MoveSpeed = 0; + private DateTime LastFlushTime = DateTime.Now; + private BattleItemWatch biw = new BattleItemWatch(); + private string CurrentNamedBattle = ""; + private string WillAppendNamedBattle = ""; + public 仙剑98DX9(GForm form) : base(form) { CoreName = "PAL98DX9"; @@ -207,6 +213,175 @@ protected override void InitCheckPoints() return false; } }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("进京城", new TimeSpan(1, 9, 32))) + { + Check = delegate () + { + //if (PositionCheck(new int[3] { 101, 256, 224 })) + if (PositionAroundCheck(101, 272, 216, 2)) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("过彩依", new TimeSpan(1, 19, 47))) + { + Check = delegate () + { + /*if (!Data.GetValue("lazhu")) + { + if (GameObj.GetItemCount(0x51) > 0) + { + Data["lazhu"] = true; + } + } + else + { + if (GameObj.GetItemCount(0x51) <= 0) + { + Data["lazhu"] = false; + return true; + } + } + return false;*/ + if (!Data.GetValue("caiyi")) + { + if (GameObj.BossID == 71) + { + Data["caiyi"] = true; + } + } + else + { + if (GameObj.BossID != 71 || (GameObj.BossID == 71 && GameObj.BattleTotalBlood <= 0)) + { + Data["caiyi"] = false; + return true; + } + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("进锁妖塔", new TimeSpan(1, 25, 33))) + { + Check = delegate () + { + //if (PositionCheck(new int[3] { 147, 1024, 448 })) + /*if (PositionAroundCheck(147, 1024, 448, 2)) + { + return true; + }*/ + if (PositionAroundCheck(164, 1024, 992, 4) || GameObj.Area == 165 || PositionAroundCheck(147, 1024, 448, 2)) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("剑柱", new TimeSpan(1, 37, 27))) + { + Check = delegate () + { + //if (PositionCheck(new int[3] { 147, 1024, 448 })) + if (PositionAroundCheck(146, 304, 1048, 3)) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("拆塔", new TimeSpan(1, 44, 22))) + { + Check = delegate () + { + if (GameObj.BossID == 144 && GameObj.BattleTotalBlood <= 0) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("过凤凰", new TimeSpan(1, 54, 11))) + { + Check = delegate () + { + if (GameObj.BossID == 67 && GameObj.BattleTotalBlood <= 0) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("进十年前", new TimeSpan(2, 3, 17))) + { + Check = delegate () + { + if (PositionAroundCheck(247, 1408, 1584, 5)) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("水灵珠", new TimeSpan(2, 14, 1))) + { + Check = delegate () + { + if (GameObj.GetItemCount(0x109) > 0) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("祈雨", new TimeSpan(2, 27, 8))) + { + Check = delegate () + { + if (PositionCheck(new int[3] { 228, 992, 928 })) + { + return true; + } + return false; + } + }); + CheckPoints.Add(new CheckPoint(CheckPoints.Count, GetBest("通关", new TimeSpan(2, 37, 32))) + { + Check = delegate () + { + if (GameObj.BossID == 149 && GameObj.BattleTotalBlood <= 0) + { + return true; + } + return false; + } + }); + } + + private bool PositionCheck(params int[][] PositionList) + { + foreach (int[] p in PositionList) + { + if (GameObj.Area == p[0] && GameObj.X == p[1] && GameObj.Y == p[2]) + { + return true; + } + } + return false; + } + + private bool PositionAroundCheck(int Area, int X, int Y, int r = 1) + { + if (GameObj.Area == Area) + { + if (GameObj.X >= (X - 16 * r) && GameObj.X <= (X + 16 * r) + && GameObj.Y >= (Y - 8 * r) && GameObj.Y <= (Y + 8 * r)) + { + return true; + } + } + return false; } public override string GetGameVersion() @@ -240,32 +415,338 @@ public override void Reset() MaxTLF = 0; BattleLong = new TimeSpan(0); ST.Reset(); + WillAppendNamedBattle = ""; NamedBattleRes = new List(); } - protected override void Checking() + public override bool NeedBlockCtrlEnter() + { + return false; + } + + public override string GetMoreInfo() + { + if (IsShowSpeed) + { + return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + MaxHCG + " 血" + MaxXLL + " 夜" + MaxYXY + " 剑" + MaxLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : ""); + } + else + { + return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + MaxHCG + " 血" + MaxXLL + " 夜" + MaxYXY + " 剑" + MaxLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : ""); + } + } + + public override string GetSmallWatch() + { + return BattleLong.TotalSeconds.ToString("F2") + "s"; + } + + public override string GetSecondWatch() + { + if (ST.CurrentTSOnly.Ticks == 0) + { + return ""; + } + return ST.ToString(); + } + + public override TimeSpan GetMainWatch() + { + return MT.CurrentTS; + } + + public override bool IsMainWatchStar() + { + return IsInUnCheat; + } + + public override string GetPointEnd() + { + return TS2HMMSSMS(WillClear); + } + + public override string GetPointSpan() + { + if (PointSpanName != "") + { + return PointSpanName + " " + GetPointSpanStr(); + } + return ""; + } + + private string GetPointSpanStr() + { + return TS2HMMSSMS(PointSpan); + } + + public override string GetAAction() + { + if (WillAppendNamedBattle == "") + { + return ""; + } + else + { + NamedBattleRes.Add(WillAppendNamedBattle); + string res = WillAppendNamedBattle; + WillAppendNamedBattle = ""; + return res; + } + } + + public override string GetCriticalError() + { + if (cryerror == "") + { + return ""; + } + else + { + string tmp = cryerror; + cryerror = ""; + return tmp; + } + } + + private ToolStripMenuItem btnCloudSave; + private ToolStripMenuItem btnCloudLoad; + private ToolStripMenuItem btnSwitch; + private ToolStripMenuItem btnGameSpeedShow; + + public override void InitUI() + { + var btnExportCurrent = form.NewMenuItem(); + btnExportCurrent.Text = "导出本次成绩"; + btnExportCurrent.Click += delegate(object sender, EventArgs e) { + ExportCurrent(GetRStr()); + }; + + var btnSetCurrentToBest = form.NewMenuItem(); + btnSetCurrentToBest.Text = "设置本次成绩为最佳"; + btnSetCurrentToBest.Click += delegate (object sender, EventArgs e) { + SaveBest(GetRStr()); + }; + + var btnJLSave = form.NewMenuItem(); + btnJLSave.Text = "接力-存档"; + btnJLSave.Click += delegate (object sender, EventArgs e) { + UI_SaveGameEx(form,delegate() { + form.Success("存档已导出到计时器目录下SRPG.bin"); + }); + }; + + var btnJLLoad = form.NewMenuItem(); + btnJLLoad.Text = "接力-接盘"; + btnJLLoad.Click += delegate (object sender, EventArgs e) { + try + { + LoadGame(); + } + catch (Exception ex) + { + form.Error(ex.Message); + return; + } + SetUIPause(true); + InfoShow isw = null; + isw = new InfoShow(form, delegate () + { + isw.Dispose(); + }); + isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中"进度一"后关闭此窗口"; + isw.btnOK.Text = "我已读档"; + isw.ShowDialog(form); + SetUIPause(false); + }; + + btnSwitch= form.NewMenuItem(); + btnSwitch.Text = "切换至简版"; + btnSwitch.Click += delegate (object sender, EventArgs e) { + if (form.Confirm("更换内核将会重置计时器,确认么?")) + { + LoadCore(new 简版(form)); + } + }; + + btnGameSpeedShow = form.NewMenuItem(); + btnGameSpeedShow.Text = "显示游戏速度"; + btnGameSpeedShow.Checked = false; + btnGameSpeedShow.Click += delegate (object sender, EventArgs e) { + btnGameSpeedShow.Checked = !btnGameSpeedShow.Checked; + }; + btnGameSpeedShow.CheckedChanged += delegate (object sender, EventArgs e) { + IsShowSpeed = btnGameSpeedShow.Checked; + }; + + btnCloudSave = form.NewCloudMenuItem(); + btnCloudSave.Text = "云存档"; + btnCloudSave.Enabled = false; + btnCloudSave.Click += delegate (object sender, EventArgs e) { + string tn = GetTimeName(); + string fn = tn + ".bin"; + UI_SaveGameEx(form,delegate () + { + btnCloudSave.Enabled = false; + Upload uw = new Upload(btnCloudSave); + uw.btnOK.Enabled = false; + uw.txtStatus.Text = "正在上传..."; + + FormEx.Run(delegate () + { + try + { + form.OUpload(System.Environment.CurrentDirectory + "\\" + fn); + if (File.Exists(System.Environment.CurrentDirectory + "\\" + fn)) + { + File.Delete(System.Environment.CurrentDirectory + "\\" + fn); + } + form.UI(delegate () + { + btnCloudSave.Enabled = true; + uw.txtStatus.Text = tn; + uw.btnOK.Enabled = true; + }); + } + catch (Exception ex) + { + form.UI(delegate () + { + btnCloudSave.Enabled = true; + uw.btnOK.Enabled = true; + uw.Dispose(); + form.Error(ex.Message); + }); + } + }); + uw.ShowDialog(form); + + }, fn); + }; + + btnCloudLoad = form.NewCloudMenuItem(); + btnCloudLoad.Text = "云读档"; + btnCloudLoad.Enabled = false; + btnCloudLoad.Click += delegate (object sender, EventArgs e) { + Download dw = new Download(delegate(string c,Download d) { + LoadCloudSRPG(form, c, d); + }); + dw.ShowDialog(form); + }; + } + + protected override void OnTick() { if (GetPalHandle()) { - if (cryerror != "") + CopyRPGIfHas(); + + JudgePause(); + try + { + FlushGameObject(); + } + catch (Exception ex) + { + } + + + try { - Error(cryerror); - cryerror = ""; + if (GameObj.Enemies.Count > 0) + { + if (!IsInBattle) + { + BattleBegin(); + } + IsInBattle = true; + IsDoMoreEndBattle = true; + Battling(); + } + else + { + if (!IsDoMoreEndBattle) + { + BattleEndMore(); + IsDoMoreEndBattle = true; + } + if (IsInBattle) + { + BattleEnd(); + IsDoMoreEndBattle = false; + } + IsInBattle = false; + } + } + catch { } + + if (HasStartGame()) + { + ST.Stop(); + if (!_IsFirstStarted) + { + _IsFirstStarted = true; + } + if (!HasUnCheated) + { + if (!IsInUnCheat) + { + CheckCheatBegin(); + CheckCheatEnd(); + } + else + { + CheckCheatEnd(); + } + } + + if (IsInUnCheat) + { + MT.Stop(); + } + else + { + MT.Start(); + Checking(); + } + } + else + { + MT.Stop(); } } else { - if (cryerror != "") + _HasGameStart = false; + MT.Stop(); + + if (_IsFirstStarted) { - Error(cryerror); - cryerror = ""; + ST.Start(); } - return; } - if (PID == -1) return; + PreData(); + } - base.Checking(); + private void PreData() + { + SI.ins.MT = MT.ToString(); + SI.ins.ST = ST.ToString(); + SI.ins.MoreInfo = this.GetMoreInfo(); + SI.ins.GameVersion = this.GetGameVersion(); + SI.ins.Version = GForm.CurrentVersion; + SI.ins.BattleLong= BattleLong.TotalSeconds.ToString("F2") + "s"; + SI.ins.FC = MaxFC.ToString(); + SI.ins.FM = MaxFM.ToString(); + SI.ins.HCG = MaxHCG.ToString(); + SI.ins.XLL = MaxXLL.ToString(); + SI.ins.YXY = MaxYXY.ToString(); + SI.ins.LQJ = MaxLQJ.ToString(); + SI.ins.CloudID = form.CloudID().ToString(); + SI.ins.CurrentStep = CurrentStep; + SI.ins.cps = CheckPoints; + SI.ins.Luck = MConfig.ins.Luck(); + SI.ins.ColorEgg = MConfig.ins.ColorEgg; } private bool GetPalHandle() @@ -395,5 +876,286 @@ private string GetGameFilePath(string fn) } return palpath; } + + private void CopyRPGIfHas() + { + if (WillCopyRPG == "") return; + + try + { + if (File.Exists(WillCopyRPG)) + { + string palpath = PalProcess.MainModule.FileName; + string[] spli = palpath.Split('\\'); + spli[spli.Length - 1] = "1.RPG"; + palpath = ""; + foreach (string s in spli) + { + palpath += s + "\\"; + } + if (palpath != "") + { + palpath = palpath.Substring(0, palpath.Length - 1); + } + if (File.Exists(palpath)) + { + if (File.Exists(palpath + ".bak")) + { + File.Delete(palpath + ".bak"); + } + File.Move(palpath, palpath + ".bak"); + } + File.Move(WillCopyRPG, palpath); + } + } + catch { } + + WillCopyRPG = ""; + } + + private void JudgePause() + { + if (IsUIPause) + { + IsPause = true; + return; + } + IntPtr hWnd = User32.GetForegroundWindow(); //获取活动窗口句柄 + int calcID = 0; //进程ID + int calcTD = 0; //线程ID + calcTD = User32.GetWindowThreadProcessId(hWnd, out calcID); + if (calcID == PID) + { + IsPause = false; + } + else + { + IsPause = true; + } + } + + private void FlushGameObject() + { + short lastx = GameObj.X; + short lasty = GameObj.Y; + short lastarea = GameObj.Area; + + GameObj.Flush(PalHandle, PID, 0, 0); + FlushPlugins(PalHandle, PID, 0, 0); + + try + { + float speed = 0; + if (GameObj.Area == lastarea) + { + DateTime now = DateTime.Now; + if (now.Second % 5 == 0) + { + MoveSpeed = 0; + } + TimeSpan ts = now - LastFlushTime; + LastFlushTime = now; + short xslot = (short)(Math.Abs(GameObj.X - lastx) / 16); + short yslot = (short)(Math.Abs(GameObj.Y - lasty) / 8); + short scha = xslot; + if (yslot > scha) + { + scha = yslot; + } + float muti = (float)(1000 / ts.TotalMilliseconds); + speed = muti * scha; + } + else + { + speed = 0; + } + if (speed > MoveSpeed) + { + MoveSpeed = speed; + } + } + catch + { } + } + + private void BattleBegin() + { + BattleLong = new TimeSpan(0); + InBattleTime = DateTime.Now; + biw = new BattleItemWatch(); + if (CurrentStep <= 5) + { + //战斗前记录下个数 + biw.Insert(0x73, GameObj.GetItemCount(0x73));//蜂 + biw.Insert(0x83, GameObj.GetItemCount(0x83));//蜜 + biw.Insert(0x8F, GameObj.GetItemCount(0x8F));//火 + } + biw.Insert(0xB8, GameObj.GetItemCount(0xB8));//龙泉剑 + biw.Insert(0xA2, GameObj.GetItemCount(0xA2));//血玲珑 + biw.Insert(0xD4, GameObj.GetItemCount(0xD4));//夜行衣 + + biw.Insert(0x47, GameObj.GetItemCount(0x47));//土灵符 + biw.Insert(0xD5, GameObj.GetItemCount(0xD5));//青铜甲 + CurrentNamedBattle = GameObj.GetNamedBattle(); + } + + private void Battling() + { + BattleLong = DateTime.Now - InBattleTime; + /*if (CurrentStep <= 5) + { + //战斗中每隔100毫秒算下差 + biw.SetCount(GameObj); + }*/ + biw.SetCount(GameObj); + } + + private void BattleEnd() + { + OutBattleTime = DateTime.Now; + BattleLong = OutBattleTime - InBattleTime; + /*if (CurrentStep <= 5) + { + //战斗结束,强制再算差 + biw.SetCount(GameObj); + }*/ + biw.SetCount(GameObj); + + if (CurrentNamedBattle != "") + { + WillAppendNamedBattle = CurrentNamedBattle + ":" + BattleLong.TotalSeconds.ToString("F2") + "s"; + CurrentNamedBattle = ""; + } + } + + private void BattleEndMore() + { + //战斗结束,强制再算差 + biw.SetCount(GameObj); + if (CurrentStep <= 5) + { + //将算出来的差加入显示 + MaxFC += biw.GettedCount(0x73); + MaxFM += biw.GettedCount(0x83); + MaxHCG += biw.GettedCount(0x8F); + } + MaxLQJ += biw.GettedCount(0xB8); + MaxXLL += biw.GettedCount(0xA2); + MaxYXY += biw.GettedCount(0xD4); + + MaxTLF += biw.GettedCount(0x47); + MaxQTJ += biw.GettedCount(0xD5); + } + + private bool HasStartGame() + { + if (!_HasGameStart) + { + if (GameObj.Area != 0) + { + _HasGameStart = true; + if (IsPause) + { + return false; + } + return true; + } + else + { + return false; + } + } + else + { + if (IsPause) + { + return false; + } + return true; + } + } + + protected override void FillMoreTimerData(HObj exdata) + { + exdata["Idle"] = ST.ToFullString(); + exdata["Lite"] = LT.ToFullString(); + exdata["BeeHouse"] = MaxFC; + exdata["BeeSheet"] = MaxFM; + exdata["FireWorm"] = MaxHCG; + exdata["DragonSword"] = MaxLQJ; + exdata["BloodLink"] = MaxXLL; + exdata["NightCloth"] = MaxYXY; + exdata["EarthPaper"] = MaxTLF; + exdata["CuArmor"] = MaxQTJ; + exdata["GMD5"] = GMD5; + exdata["DX9Version"] = DX9Version; + + string namedbattles = ""; + foreach (string nmb in NamedBattleRes) + { + namedbattles += nmb + "|"; + } + if (namedbattles != "") + { + namedbattles = namedbattles.Substring(0, namedbattles.Length - 1); + } + exdata["NamedBattles"] = namedbattles; + } + + private void CheckCheatBegin() + { + if (PositionCheck(new int[3] { 177, 1088, 608 }, new int[3] { 177, 1120, 608 }, new int[3] { 177, 1120, 592 })) + { + IsInUnCheat = true; + } + } + + private void CheckCheatEnd() + { + if (GameObj.GetItemCount(0x123) > 0) + { + IsInUnCheat = false; + HasUnCheated = true; + } + } + + public override void OnFunctionKey(int FunNo) + { + switch (FunNo) + { + case 8: + HandPause(); + break; + case 6: + if (form.Confirm("更换内核将会重置计时器,确认么?")) + { + LoadCore(new 简版(form)); + } + break; + } + } + + public override void OnCloudOK() + { + btnCloudSave.Enabled = true; + btnCloudLoad.Enabled = true; + } + + public override void OnCloudFail() + { + btnCloudSave.Enabled = false; + btnCloudLoad.Enabled = false; + } + + public override void OnCloudPending() + { + btnCloudSave.Enabled = false; + btnCloudLoad.Enabled = false; + } + + public override string ForCloudLiteData() + { + return MT.CurrentTS.Ticks.ToString() + "," + ST.CurrentTS.Ticks.ToString() + "," + BattleLong.Ticks.ToString(); + } } } From 241de7adec87e7370a8446c5f842d84bdb38f4ce Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 31 Jan 2026 23:17:59 +0800 Subject: [PATCH 03/24] Fix string formatting in import success message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces double quotes with single quotes around '进度一' in the import success message for consistency and to avoid nested quote issues. --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 996b7bc..9ad8374 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -553,7 +553,7 @@ public override void InitUI() { isw.Dispose(); }); - isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中"进度一"后关闭此窗口"; + isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中'进度一'后关闭此窗口"; isw.btnOK.Text = "我已读档"; isw.ShowDialog(form); SetUIPause(false); From d0bef5dd32dc7734c5f9b4a64bd8056cb8451285 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:22:33 +0000 Subject: [PATCH 04/24] Fix missing methods: Replace TS2HMMSSMS and add save/load functionality Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 347 +++++++++++++++++- 1 file changed, 336 insertions(+), 11 deletions(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 9ad8374..c7aad8a 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -462,21 +462,13 @@ public override bool IsMainWatchStar() public override string GetPointEnd() { - return TS2HMMSSMS(WillClear); + return "预计通关 " + GetWillClearStr(); } public override string GetPointSpan() { - if (PointSpanName != "") - { - return PointSpanName + " " + GetPointSpanStr(); - } - return ""; - } - - private string GetPointSpanStr() - { - return TS2HMMSSMS(PointSpan); + if (PointSpanName == "") return "--"; + return PointSpanName + " " + GetPointSpanStr(); } public override string GetAAction() @@ -508,6 +500,339 @@ public override string GetCriticalError() } } + public delegate void OnExSuccess(); + public delegate void ExEnd(bool IsOK, string ErrStr); + + private bool IsListenSave = false; + private string[] tnbase = new string[60] { + "0","1","2","3","4","5","6","7","8","9", + "A","B","C","D","E","F","G","H","I","J", + "K","L","M","N","P","Q","R","S","T", + "U","V","W","X","Y","Z","a","b","c","d", + "e","f","g","h","i","j","k","m","n", + "o","p","q","r","s","t","u","v","w","x", + "y","z" + }; + + private void SuspendSaveListen() + { + IsListenSave = false; + } + + private string GetPalFolder() + { + string palpath = PalProcess.MainModule.FileName; + string[] spli = palpath.Split('\\'); + spli[spli.Length - 1] = ""; + palpath = ""; + foreach (string s in spli) + { + palpath += s + "\\"; + } + if (palpath != "") + { + palpath = palpath.Substring(0, palpath.Length - 1); + } + return palpath + "\\"; + } + + private void UI_SaveGameEx(FormEx f,OnExSuccess cb, string fn = "SRPG.bin") + { + SetUIPause(true); + InfoShow isw = null; + isw = new InfoShow(f, delegate () + { + SuspendSaveListen(); + SetUIPause(false); + isw.Dispose(); + }); + isw.lblInfo.Text = "计时器已暂停,请在游戏中存档"; + bool haserr = false; + try + { + SaveGameEx(f, delegate (bool isok, string errstr) + { + if (isok) + { + if (cb != null) + { + cb(); + } + } + else + { + if (errstr != "") + { + f.Error(errstr); + } + else + { + f.Alert("操作中断"); + } + } + SetUIPause(false); + isw.Dispose(); + }, fn); + } + catch (Exception ex) + { + haserr = true; + f.Error(ex.Message); + } + if (haserr) + { + if (isw != null) + { + isw.Dispose(); + } + SetUIPause(false); + } + else + { + isw.ShowDialog(f); + } + } + + private void SaveGameEx(FormEx f,ExEnd cb, string fn = "SRPG.bin") + { + if (!GetPalHandle()) throw new Exception("游戏没有在运行,无法保存"); + IsListenSave = true; + FormEx.Run(delegate () + { + Dictionary RPGs = new Dictionary(); + string palfolder = GetPalFolder(); + for (int i = 1; i <= 5; ++i) + { + string p = palfolder + i + ".RPG"; + if (File.Exists(p)) + { + FileInfo fi = new FileInfo(p); + RPGs.Add(i, fi.LastWriteTime); + } + } + Thread.Sleep(300); + string ChangedFile = ""; + while (IsListenSave && ChangedFile == "") + { + for (int i = 0; i <= 5; ++i) + { + string p = palfolder + i + ".RPG"; + if (File.Exists(p)) + { + if (RPGs.ContainsKey(i)) + { + FileInfo fi = new FileInfo(p); + if (fi.LastWriteTime > RPGs[i]) + { + ChangedFile = p; + break; + } + } + else + { + ChangedFile = p; + break; + } + } + } + Thread.Sleep(300); + } + + if (!IsListenSave) + { + if (cb != null) + { + f.UI(delegate () + { + cb(false, ""); + }); + } + return; + } + + SRPGobj so = new SRPGobj(); + so.RPG = SaveObject.GetSaveBuffer(this.PalHandle); + so.TimerStr = GetRStr(); + + string FilePath = fn; + try + { + if (File.Exists(FilePath)) + { + File.Delete(FilePath); + } + using (FileStream fs = new FileStream(FilePath, FileMode.OpenOrCreate)) + { + BinaryFormatter bf = new BinaryFormatter(); + bf.Serialize(fs, so); + } + } + catch (Exception ex) + { + if (cb != null) + { + f.UI(delegate () + { + cb(false, ex.Message); + }); + } + } + if (cb != null) + { + f.UI(delegate () + { + cb(true, ""); + }); + } + }); + } + + private void LoadGame(string fn = "SRPG.bin", string rn = "1.RPG") + { + SRPGobj so = null; + string FilePath = fn; + try + { + if (!File.Exists(FilePath)) throw new Exception("计时器目录下找不到" + fn); + using (FileStream fs = new FileStream(FilePath, FileMode.OpenOrCreate)) + { + fs.Seek(0, SeekOrigin.Begin); + BinaryFormatter bf = new BinaryFormatter(); + so = bf.Deserialize(fs) as SRPGobj; + } + } + catch + { + throw; + } + + if (so != null) + { + string tmppath = rn; + try + { + if (File.Exists(tmppath)) + { + File.Delete(tmppath); + } + using (FileStream fileStream = new FileStream(tmppath, FileMode.OpenOrCreate)) + { + using (BinaryWriter Writer = new BinaryWriter(fileStream)) + { + Writer.Write(so.RPG); + Writer.Flush(); + } + } + } + catch + { + throw; + } + + SetTimerFromString(so.TimerStr); + + WillCopyRPG = tmppath; + } + } + + public void SetTimerFromString(string json) + { + HObj ho = new HObj(json); + try + { + MaxFC = ho.GetValue("BeeHouse"); + MaxFM = ho.GetValue("BeeSheet"); + MaxHCG = ho.GetValue("FireWorm"); + MaxLQJ = ho.GetValue("DragonSword"); + MaxXLL = ho.GetValue("BloodLink"); + MaxYXY = ho.GetValue("NightCloth"); + MaxTLF = ho.GetValue("EarthPaper"); + MaxQTJ = ho.GetValue("CuArmor"); + MT.SetTS(ConvertTimeSpan(ho.GetValue("Current"))); + ST.SetTS(ConvertTimeSpan(ho.GetValue("Idle"))); + HObj cps = ho.GetValue("CheckPoints"); + for (int i = 0; i < cps.Count; ++i) + { + HObj cc = cps.GetValue(i); + CheckPoints[i].SetCurrentTSForLoad(ConvertTimeSpan(cc.GetValue("time"))); + } + Jump(ho.GetValue("Step")); + string nmbs= ho.GetValue("NamedBattles"); + NamedBattleRes = new List(); + string[] nmbspli = nmbs.Split('|'); + foreach (string nmb in nmbspli) + { + NamedBattleRes.Add(nmb); + } + WillAppendNamedBattle = nmbs; + } + catch + { + throw; + } + } + + private string GetTimeName() + { + if (form.CloudID() < 0) throw new Exception("云功能没有初始化"); + string res = form.CloudID().ToString().PadLeft(3, '0'); + DateTime now = DateTime.Now; + res += tnbase[now.Month] + tnbase[now.Day] + tnbase[now.Hour] + tnbase[now.Minute] + tnbase[now.Second]; + return res; + } + + public void LoadCloudSRPG(FormEx f,string code, Download dw) + { + FormEx.Run(delegate () { + try + { + string key = code + ".bin"; + string localname = System.Environment.CurrentDirectory + "\\" + key; + form.ODownload(key, localname); + f.UI(delegate () + { + try + { + LoadGame(key); + if (File.Exists(localname)) + { + File.Delete(localname); + } + dw.txtCode.Enabled = true; + dw.btnOK.Enabled = true; + dw.Dispose(); + + SetUIPause(true); + InfoShow isw = null; + isw = new InfoShow(f, delegate () + { + isw.Dispose(); + }); + isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中"进度一"后关闭此窗口"; + isw.btnOK.Text = "我已读档"; + isw.ShowDialog(f); + SetUIPause(false); + } + catch (Exception ee) + { + dw.txtCode.Enabled = true; + dw.btnOK.Enabled = true; + f.Error(ee.Message); + } + }); + } + catch (Exception ex) + { + f.UI(delegate () + { + dw.txtCode.Enabled = true; + dw.btnOK.Enabled = true; + f.Error(ex.Message); + }); + } + }); + } + private ToolStripMenuItem btnCloudSave; private ToolStripMenuItem btnCloudLoad; private ToolStripMenuItem btnSwitch; From 6859c11de8f6f120983d51ec8919bd0c022ff82a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:29:29 +0000 Subject: [PATCH 05/24] =?UTF-8?q?Fix=20DX9=20version=20detection=20and=20a?= =?UTF-8?q?dd=20=E4=BB=99=E5=89=9198=20prefix=20to=20version=20display?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index c7aad8a..08758f1 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -388,7 +388,7 @@ public override string GetGameVersion() { if (PID != -1) { - return "新补丁 " + DX9Version; + return "仙剑98 新补丁 " + DX9Version; } else { @@ -1109,7 +1109,8 @@ private bool GetPalHandle() string windowTitle = sb.ToString(); // 功能1: 窗口标题识别 - if (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) + if ((windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || + (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9"))) { // 提取版本号 int versionStartIndex = windowTitle.IndexOf("(v"); From e85c66e60a7381f0bd522147f0e87daa1ea1cb51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:39:27 +0000 Subject: [PATCH 06/24] Add grace period for PAL.DLL to modify window title for DX9 detection Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 08758f1..a5136b3 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -74,6 +74,10 @@ public override bool IsShowC() private string CurrentNamedBattle = ""; private string WillAppendNamedBattle = ""; + private DateTime? InitialDetectionTime = null; // 首次检测到游戏的时间 + private bool HasConfirmedDX9 = false; // 是否已确认DX9标题 + private const int DX9TitleGracePeriodSeconds = 10; // DX9标题出现的宽限期(秒) + public 仙剑98DX9(GForm form) : base(form) { CoreName = "PAL98DX9"; @@ -417,6 +421,8 @@ public override void Reset() ST.Reset(); WillAppendNamedBattle = ""; NamedBattleRes = new List(); + InitialDetectionTime = null; + HasConfirmedDX9 = false; } public override bool NeedBlockCtrlEnter() @@ -1108,11 +1114,18 @@ private bool GetPalHandle() User32.GetWindowText(tempHandle, sb, sb.Capacity); string windowTitle = sb.ToString(); - // 功能1: 窗口标题识别 - if ((windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || - (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9"))) + // 检查是否包含DX9标识 + bool hasDX9Title = (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || + (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9")); + + // 检查是否是基础游戏标题(PAL.DLL还未修改标题) + bool isBaseGameTitle = windowTitle.Contains("仙剑奇侠传") || + windowTitle.StartsWith("PAL98") || + windowTitle.StartsWith("Pal98"); + + if (hasDX9Title) { - // 提取版本号 + // 找到DX9标题,提取版本号并连接 int versionStartIndex = windowTitle.IndexOf("(v"); if (versionStartIndex != -1) { @@ -1128,10 +1141,36 @@ private bool GetPalHandle() PID = PalProcess.Id; PalHandle = new IntPtr(Kernel32.OpenProcess(0x1F0FFF, false, PID)); CalcPalMD5(); + HasConfirmedDX9 = true; + InitialDetectionTime = null; // 重置初始检测时间 return true; } + else if (isBaseGameTitle) + { + // 检测到基础游戏标题,给PAL.DLL时间加载和修改标题 + if (InitialDetectionTime == null) + { + InitialDetectionTime = DateTime.Now; + } + + TimeSpan elapsedTime = DateTime.Now - InitialDetectionTime.Value; + + if (elapsedTime.TotalSeconds < DX9TitleGracePeriodSeconds) + { + // 在宽限期内,暂时接受,并继续检测 + // 但不设置PID,这样下次还会重新检查标题 + return false; // 返回false但不设置错误,继续等待 + } + else + { + // 超过宽限期仍未出现DX9标题,显示错误 + cryerror = "请使用仙剑98 DX9移植版!检测到基础游戏但未找到DX9标识"; + return false; + } + } else { + // 既不是DX9标题也不是基础游戏标题 cryerror = "请使用仙剑98 DX9移植版!"; return false; } @@ -1140,6 +1179,33 @@ private bool GetPalHandle() { if (PID == res[0].Id) { + // 已连接到游戏,但如果还没确认DX9,继续检查标题 + if (!HasConfirmedDX9) + { + IntPtr tempHandle = res[0].MainWindowHandle; + StringBuilder sb = new StringBuilder(256); + User32.GetWindowText(tempHandle, sb, sb.Capacity); + string windowTitle = sb.ToString(); + + bool hasDX9Title = (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || + (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9")); + + if (hasDX9Title) + { + // 提取版本号 + int versionStartIndex = windowTitle.IndexOf("(v"); + if (versionStartIndex != -1) + { + int versionEndIndex = windowTitle.IndexOf(")", versionStartIndex); + if (versionEndIndex != -1) + { + DX9Version = windowTitle.Substring(versionStartIndex + 2, versionEndIndex - versionStartIndex - 2); + } + } + HasConfirmedDX9 = true; + } + } + if (GMD5 == "none") { CalcPalMD5(); @@ -1154,6 +1220,8 @@ private bool GetPalHandle() PID = -1; GMD5 = "none"; DX9Version = "未知"; + InitialDetectionTime = null; + HasConfirmedDX9 = false; return false; } } @@ -1166,6 +1234,8 @@ private bool GetPalHandle() PID = -1; GMD5 = "none"; DX9Version = "未知"; + InitialDetectionTime = null; + HasConfirmedDX9 = false; return false; } } From a4ab4511302e3f45f4c5897cb1063646bd2e4561 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:49:49 +0000 Subject: [PATCH 07/24] Fix window handle transition issue - handle sdf window and empty titles Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index a5136b3..496d839 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -1110,18 +1110,62 @@ private bool GetPalHandle() if (PID == -1) { IntPtr tempHandle = res[0].MainWindowHandle; + + // 处理窗口句柄可能为空的情况(窗口转换期间) + if (tempHandle == IntPtr.Zero) + { + // 窗口正在转换,给予宽限期 + if (InitialDetectionTime == null) + { + InitialDetectionTime = DateTime.Now; + } + + TimeSpan elapsedTime = DateTime.Now - InitialDetectionTime.Value; + if (elapsedTime.TotalSeconds < DX9TitleGracePeriodSeconds) + { + return false; // 继续等待,不显示错误 + } + else + { + cryerror = "请使用仙剑98 DX9移植版!窗口句柄无效"; + return false; + } + } + StringBuilder sb = new StringBuilder(256); User32.GetWindowText(tempHandle, sb, sb.Capacity); string windowTitle = sb.ToString(); + // 处理窗口标题为空或仅包含空白字符的情况(窗口转换期间) + if (string.IsNullOrWhiteSpace(windowTitle)) + { + // 窗口标题为空,可能正在转换 + if (InitialDetectionTime == null) + { + InitialDetectionTime = DateTime.Now; + } + + TimeSpan elapsedTime = DateTime.Now - InitialDetectionTime.Value; + if (elapsedTime.TotalSeconds < DX9TitleGracePeriodSeconds) + { + return false; // 继续等待,不显示错误 + } + else + { + cryerror = "请使用仙剑98 DX9移植版!无法获取窗口标题"; + return false; + } + } + // 检查是否包含DX9标识 bool hasDX9Title = (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9")); - // 检查是否是基础游戏标题(PAL.DLL还未修改标题) + // 检查是否是基础游戏标题(PAL.DLL还未修改标题,或VB4初始窗口) bool isBaseGameTitle = windowTitle.Contains("仙剑奇侠传") || windowTitle.StartsWith("PAL98") || - windowTitle.StartsWith("Pal98"); + windowTitle.StartsWith("Pal98") || + windowTitle.Equals("sdf", StringComparison.OrdinalIgnoreCase); // VB4初始窗口 if (hasDX9Title) { From 9d29ddfa22b0bc017406212effa2083bf558a136 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 1 Feb 2026 00:43:37 +0800 Subject: [PATCH 08/24] Update version to 3.35, improve DX9 patch support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump version to 3.35.0 and update assembly info. Enhance DX9 patch detection logic to support new window title patterns and versions. Add 'othercat' to AboutForm contributors. Fix typos in UI text from '改建器' to '改键器'. Suppress some error messages for multiple Pal.exe processes and missing DX9 patch. Minor UI string fix for archive import dialog. --- KeyChanger/MainForm.Designer.cs | 4 +-- KeyChanger/MainForm.cs | 4 +-- Pal98Timer/AboutForm.cs | 3 ++ Pal98Timer/GForm.cs | 4 +-- Pal98Timer/KeyChangerDel.cs | 2 +- Pal98Timer/Properties/AssemblyInfo.cs | 4 +-- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 36 ++++++++++++++++--- 7 files changed, 43 insertions(+), 14 deletions(-) diff --git a/KeyChanger/MainForm.Designer.cs b/KeyChanger/MainForm.Designer.cs index 6a9b41c..3f2e5bd 100644 --- a/KeyChanger/MainForm.Designer.cs +++ b/KeyChanger/MainForm.Designer.cs @@ -51,7 +51,7 @@ private void InitializeComponent() // this.niMain.ContextMenuStrip = this.cmMain; this.niMain.Icon = ((System.Drawing.Icon)(resources.GetObject("niMain.Icon"))); - this.niMain.Text = "改建器 by Houou"; + this.niMain.Text = "改键器 by Houou"; this.niMain.Visible = true; this.niMain.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.niMain_MouseDoubleClick); // @@ -150,7 +150,7 @@ private void InitializeComponent() this.Name = "MainForm"; this.Opacity = 0D; this.ShowInTaskbar = false; - this.Text = "改建器"; + this.Text = "改键器"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); this.Load += new System.EventHandler(this.MainForm_Load); this.Shown += new System.EventHandler(this.MainForm_Shown); diff --git a/KeyChanger/MainForm.cs b/KeyChanger/MainForm.cs index ef837b6..45de506 100644 --- a/KeyChanger/MainForm.cs +++ b/KeyChanger/MainForm.cs @@ -74,7 +74,7 @@ public MainForm() ShowKCEnable(); ShowBCEEnable(); - niMain.ShowBalloonTip(1000, "改建器", "已启动,双击图标打开按键展示,右键设置", ToolTipIcon.Info); + niMain.ShowBalloonTip(1000, "改键器", "已启动,双击图标打开按键展示,右键设置", ToolTipIcon.Info); try { initForPaint(); @@ -400,7 +400,7 @@ private void MainForm_Shown(object sender, EventArgs e) private void Exit() { - niMain.ShowBalloonTip(1000, "改建器", "已退出", ToolTipIcon.Warning); + niMain.ShowBalloonTip(1000, "改键器", "已退出", ToolTipIcon.Warning); _keyboardHook.UninstallHook(); niMain.Dispose(); Environment.Exit(0); diff --git a/Pal98Timer/AboutForm.cs b/Pal98Timer/AboutForm.cs index 52fa1bc..badd2a1 100644 --- a/Pal98Timer/AboutForm.cs +++ b/Pal98Timer/AboutForm.cs @@ -32,6 +32,7 @@ private Dictionary InitCreaterInfo() r.Add("齐大", new string[3] { "齐小伙", "http://www.palspeed.com/tj.html?p11", "1" }); r.Add("麻烦", new string[3] { "麻烦(鼻祖)", "http://www.palspeed.com/tj.html?p12", "1" }); r.Add("wjjjj12", new string[3] { "wjjjj12", "http://www.palspeed.com/tj.html?p13", "1" }); + r.Add("othercat", new string[3] { "othercat", "https://space.bilibili.com/349512", "1" }); r.Add("!官网", new string[3] { "www.palspeed.com", "http://www.palspeed.com", "2" }); r.Add("!各位玩家", new string[3] { "各位玩家", "http://www.palspeed.com/tj.html?n1", "2" }); r.Add("!github", new string[3] { "https://github.com/ihouou/PalTimer", "https://github.com/ihouou/PalTimer", "2" }); @@ -49,6 +50,8 @@ private HText InitData(Dictionary c) .Title("内核贡献") .Text("仙剑98柔情:").Link(c["麻烦"]).Space().Link(c["!各位玩家"]) .Line() + .Text("仙剑98 DX9新补丁:").Link(c["othercat"]) + .Line() .Text("仙剑98 Steam:").Link(c["寒泠"]) .Line() .Text("仙剑二 Steam:").Link(c["齐大"]).Space().Link(c["寒泠"]).Line().Space(14).Link(c["发财鼠"]).Space().Link(c["wjjjj12"]) diff --git a/Pal98Timer/GForm.cs b/Pal98Timer/GForm.cs index 973d6c7..2126586 100644 --- a/Pal98Timer/GForm.cs +++ b/Pal98Timer/GForm.cs @@ -12,7 +12,7 @@ namespace Pal98Timer { public partial class GForm : NoneBoardFormEx { - public const string CurrentVersion = "3.34.1"; + public const string CurrentVersion = "3.35.0"; public const string bgpath = @"bg.png"; private TimerCore core; private bool IsAutoLuck = false; @@ -130,7 +130,7 @@ public GForm():base(true) } catch (Exception ex) { - LoadCore(new 仙剑98柔情(this)); + LoadCore(new 仙剑98DX9(this)); } rr.SetVersion(CurrentVersion); diff --git a/Pal98Timer/KeyChangerDel.cs b/Pal98Timer/KeyChangerDel.cs index 1cdc3df..c1cb275 100644 --- a/Pal98Timer/KeyChangerDel.cs +++ b/Pal98Timer/KeyChangerDel.cs @@ -25,7 +25,7 @@ public class KeyChangerDel private const string tar = "KeyChanger"; private static int call(int act,int data=0) { - int hwnd = FindWindow(null, "改建器"); + int hwnd = FindWindow(null, "改键器"); if (hwnd != 0) { return SendMessage(hwnd, TIMERCALL, (IntPtr)act, (IntPtr)data).ToInt32(); diff --git a/Pal98Timer/Properties/AssemblyInfo.cs b/Pal98Timer/Properties/AssemblyInfo.cs index c870a75..0fe3bf5 100644 --- a/Pal98Timer/Properties/AssemblyInfo.cs +++ b/Pal98Timer/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, // 方法是按如下所示使用“*”: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.34")] -[assembly: AssemblyFileVersion("3.34")] +[assembly: AssemblyVersion("3.35")] +[assembly: AssemblyFileVersion("3.35")] diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 496d839..ce3bf65 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -814,7 +814,7 @@ public void LoadCloudSRPG(FormEx f,string code, Download dw) { isw.Dispose(); }); - isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中"进度一"后关闭此窗口"; + isw.lblInfo.Text = "存档导入成功,计时器已自动暂停,请读取游戏中\"进度一\"后关闭此窗口"; isw.btnOK.Text = "我已读档"; isw.ShowDialog(f); SetUIPause(false); @@ -1096,7 +1096,7 @@ private bool GetPalHandle() { if (!HasAlertMutiPal) { - cryerror = "检测到多个Pal.exe进程,请关闭其他的,只保留一个!"; + //cryerror = "检测到多个Pal.exe进程,请关闭其他的,只保留一个!"; HasAlertMutiPal = true; } return false; @@ -1158,7 +1158,9 @@ private bool GetPalHandle() } // 检查是否包含DX9标识 - bool hasDX9Title = (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || + bool hasDX9Title = (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) || + (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("新补丁")) || + (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("(v")) || (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9")); // 检查是否是基础游戏标题(PAL.DLL还未修改标题,或VB4初始窗口) @@ -1179,6 +1181,30 @@ private bool GetPalHandle() DX9Version = windowTitle.Substring(versionStartIndex + 2, versionEndIndex - versionStartIndex - 2); } } + else + { + int versionStartIndex_old1 = windowTitle.IndexOf("(新补丁"); + if (versionStartIndex_old1 != -1) + { + int versionEndIndex = windowTitle.IndexOf(" 测试版)", versionStartIndex_old1); + if (versionEndIndex != -1) + { + DX9Version = windowTitle.Substring(versionStartIndex_old1 + 4, versionEndIndex - versionStartIndex_old1 - 3); + } + } + else + { + int versionStartIndex_old2 = windowTitle.IndexOf("("); + if (versionStartIndex_old2 != -1) + { + int versionEndIndex = windowTitle.IndexOf(")", versionStartIndex_old2); + if (versionEndIndex != -1) + { + DX9Version = windowTitle.Substring(versionStartIndex_old2, versionEndIndex - versionStartIndex_old2 - 3); + } + } + } + } PalProcess = res[0]; GameWindowHandle = res[0].MainWindowHandle; @@ -1208,14 +1234,14 @@ private bool GetPalHandle() else { // 超过宽限期仍未出现DX9标题,显示错误 - cryerror = "请使用仙剑98 DX9移植版!检测到基础游戏但未找到DX9标识"; + //cryerror = "请使用仙剑98 新补丁DX9移植版!检测到基础游戏但未找到DX9标识"; return false; } } else { // 既不是DX9标题也不是基础游戏标题 - cryerror = "请使用仙剑98 DX9移植版!"; + //cryerror = "请使用仙剑98 新补丁DX9移植版!"; return false; } } From 43e419539262c02087c91ac62dda1d05b566d7ff Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 1 Feb 2026 12:44:31 +0800 Subject: [PATCH 09/24] Update references to new patch in about and DX9 files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed text references from '仙剑98 DX9新补丁' to '仙剑98原版 2025新补丁' in AboutForm.cs and the DX9-related source file to reflect the updated patch naming. --- Pal98Timer/AboutForm.cs | 4 ++-- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Pal98Timer/AboutForm.cs b/Pal98Timer/AboutForm.cs index badd2a1..2776575 100644 --- a/Pal98Timer/AboutForm.cs +++ b/Pal98Timer/AboutForm.cs @@ -48,9 +48,9 @@ private HText InitData(Dictionary c) .Title("视觉") .Text("界面:").Link(c["Houou"]).Space().Text("图标:").Link(c["兰"]).Space().Link(c["狐狸"]) .Title("内核贡献") - .Text("仙剑98柔情:").Link(c["麻烦"]).Space().Link(c["!各位玩家"]) + .Text("仙剑98原版 2.0/3.0补丁:").Link(c["麻烦"]).Space().Link(c["!各位玩家"]) .Line() - .Text("仙剑98 DX9新补丁:").Link(c["othercat"]) + .Text("仙剑98原版 2025新补丁:").Link(c["othercat"]) .Line() .Text("仙剑98 Steam:").Link(c["寒泠"]) .Line() diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index ce3bf65..a3cd3ef 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -392,7 +392,7 @@ public override string GetGameVersion() { if (PID != -1) { - return "仙剑98 新补丁 " + DX9Version; + return "仙剑98原版 2025新补丁 " + DX9Version; } else { From 410799b4853239a275a3e8457c0d3ad7d3a83223 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 05:04:43 +0000 Subject: [PATCH 10/24] Implement 4 new features: monster counter, real-time battle stats, single instance, KeyChanger management Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GForm.cs | 2 + Pal98Timer/Program.cs | 29 +++++++++---- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 42 +++++++++++++++---- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Pal98Timer/GForm.cs b/Pal98Timer/GForm.cs index 2126586..331d7fa 100644 --- a/Pal98Timer/GForm.cs +++ b/Pal98Timer/GForm.cs @@ -534,9 +534,11 @@ public void OnKeyPress(KeyboardLib.HookStruct hookStruct, out bool handle) if (KeyChangerDel.IsEnable()) { KeyChangerDel.Disable(); + KeyChangerDel.Close(); // 功能4:关闭KEYCHANGER.EXE进程 } else { + KeyChangerDel.Open(); // 功能4:重新打开KEYCHANGER.EXE KeyChangerDel.Enable(); } ShowKCEnable(); diff --git a/Pal98Timer/Program.cs b/Pal98Timer/Program.cs index f0aa65b..1cb2341 100644 --- a/Pal98Timer/Program.cs +++ b/Pal98Timer/Program.cs @@ -5,6 +5,7 @@ using System.Windows.Forms; using System.Diagnostics; using System.Text; +using System.Threading; namespace Pal98Timer { @@ -16,13 +17,27 @@ static class Program [STAThread] static void Main() { - ClearTmpBG(); - UpdateBestFiles(); - KeyChangerDel.Open(); - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new GForm()); - //Application.Run(new BestEditForm("CUSTOM")); + // 功能3:单实例检查 + bool createdNew; + using (Mutex mutex = new Mutex(true, "Pal98Timer_SingleInstance_Mutex", out createdNew)) + { + if (!createdNew) + { + MessageBox.Show("仙剑98计时器已经在运行中!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + ClearTmpBG(); + UpdateBestFiles(); + KeyChangerDel.Open(); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new GForm()); + //Application.Run(new BestEditForm("CUSTOM")); + + // 保持mutex直到程序退出 + GC.KeepAlive(mutex); + } } static void ClearTmpBG() { diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index a3cd3ef..d43e867 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -78,6 +78,13 @@ public override bool IsShowC() private bool HasConfirmedDX9 = false; // 是否已确认DX9标题 private const int DX9TitleGracePeriodSeconds = 10; // DX9标题出现的宽限期(秒) + private int TotalMonsterCount = 0; // 撞怪总数 + + // 战斗中实时显示用的临时变量(功能2) + private short CurrentBattleHCG = 0; // 当前战斗中获得的火虫草 + private short CurrentBattleXLL = 0; // 当前战斗中获得的血玲珑 + private short CurrentBattleLQJ = 0; // 当前战斗中获得的龙泉剑 + public 仙剑98DX9(GForm form) : base(form) { CoreName = "PAL98DX9"; @@ -423,6 +430,7 @@ public override void Reset() NamedBattleRes = new List(); InitialDetectionTime = null; HasConfirmedDX9 = false; + TotalMonsterCount = 0; // 重置撞怪计数器 } public override bool NeedBlockCtrlEnter() @@ -432,13 +440,18 @@ public override bool NeedBlockCtrlEnter() public override string GetMoreInfo() { + // 在战斗中显示实时数据(功能2) + short displayHCG = MaxHCG + (IsInBattle ? CurrentBattleHCG : (short)0); + short displayXLL = MaxXLL + (IsInBattle ? CurrentBattleXLL : (short)0); + short displayLQJ = MaxLQJ + (IsInBattle ? CurrentBattleLQJ : (short)0); + if (IsShowSpeed) { - return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + MaxHCG + " 血" + MaxXLL + " 夜" + MaxYXY + " 剑" + MaxLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : ""); + return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount.ToString("D3"); } else { - return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + MaxHCG + " 血" + MaxXLL + " 夜" + MaxYXY + " 剑" + MaxLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : ""); + return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount.ToString("D3"); } } @@ -1449,6 +1462,13 @@ private void BattleBegin() BattleLong = new TimeSpan(0); InBattleTime = DateTime.Now; biw = new BattleItemWatch(); + TotalMonsterCount++; // 撞怪计数器增加 + + // 重置战斗中实时显示的临时变量(功能2) + CurrentBattleHCG = 0; + CurrentBattleXLL = 0; + CurrentBattleLQJ = 0; + if (CurrentStep <= 5) { //战斗前记录下个数 @@ -1456,6 +1476,11 @@ private void BattleBegin() biw.Insert(0x83, GameObj.GetItemCount(0x83));//蜜 biw.Insert(0x8F, GameObj.GetItemCount(0x8F));//火 } + else + { + // 如果不在前5关,也要记录火虫草的初始值以便实时统计 + biw.Insert(0x8F, GameObj.GetItemCount(0x8F));//火 + } biw.Insert(0xB8, GameObj.GetItemCount(0xB8));//龙泉剑 biw.Insert(0xA2, GameObj.GetItemCount(0xA2));//血玲珑 biw.Insert(0xD4, GameObj.GetItemCount(0xD4));//夜行衣 @@ -1468,12 +1493,12 @@ private void BattleBegin() private void Battling() { BattleLong = DateTime.Now - InBattleTime; - /*if (CurrentStep <= 5) - { - //战斗中每隔100毫秒算下差 - biw.SetCount(GameObj); - }*/ biw.SetCount(GameObj); + + // 实时更新火、血、剑的数量(功能2) + CurrentBattleHCG = biw.GettedCount(0x8F); // 火虫草 + CurrentBattleXLL = biw.GettedCount(0xA2); // 血玲珑 + CurrentBattleLQJ = biw.GettedCount(0xB8); // 龙泉剑 } private void BattleEnd() @@ -1503,8 +1528,10 @@ private void BattleEndMore() //将算出来的差加入显示 MaxFC += biw.GettedCount(0x73); MaxFM += biw.GettedCount(0x83); + // MaxHCG 已在Battling中实时更新,这里累加最终值 MaxHCG += biw.GettedCount(0x8F); } + // MaxLQJ 和 MaxXLL 已在Battling中实时更新,这里累加最终值 MaxLQJ += biw.GettedCount(0xB8); MaxXLL += biw.GettedCount(0xA2); MaxYXY += biw.GettedCount(0xD4); @@ -1555,6 +1582,7 @@ protected override void FillMoreTimerData(HObj exdata) exdata["CuArmor"] = MaxQTJ; exdata["GMD5"] = GMD5; exdata["DX9Version"] = DX9Version; + exdata["TotalMonsterCount"] = TotalMonsterCount; // 保存撞怪总数 string namedbattles = ""; foreach (string nmb in NamedBattleRes) From ef692ba03d0c9325a56e844ca8e4c4cfb7ec778e Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 1 Feb 2026 13:27:16 +0800 Subject: [PATCH 11/24] Release v3.35.1 with 2025 patch support and fixes Update version to 3.35.1 and add support for the 2025 new patch, making it the default configuration. Adjusted data types for real-time stats, improved monster count display, and prevented multiple timer instances from running. Updated UI text, fixed key remapper naming, and revised documentation for new features and requirements. --- Pal98Timer/GForm.cs | 2 +- Pal98Timer/Program.cs | 2 +- Pal98Timer/Properties/AssemblyInfo.cs | 4 +-- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 10 +++---- README.md | 29 ++++++++++++++----- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Pal98Timer/GForm.cs b/Pal98Timer/GForm.cs index 331d7fa..b819ed6 100644 --- a/Pal98Timer/GForm.cs +++ b/Pal98Timer/GForm.cs @@ -12,7 +12,7 @@ namespace Pal98Timer { public partial class GForm : NoneBoardFormEx { - public const string CurrentVersion = "3.35.0"; + public const string CurrentVersion = "3.35.1"; public const string bgpath = @"bg.png"; private TimerCore core; private bool IsAutoLuck = false; diff --git a/Pal98Timer/Program.cs b/Pal98Timer/Program.cs index 1cb2341..c22706d 100644 --- a/Pal98Timer/Program.cs +++ b/Pal98Timer/Program.cs @@ -23,7 +23,7 @@ static void Main() { if (!createdNew) { - MessageBox.Show("仙剑98计时器已经在运行中!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show("自动计时器已经在运行中!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } diff --git a/Pal98Timer/Properties/AssemblyInfo.cs b/Pal98Timer/Properties/AssemblyInfo.cs index 0fe3bf5..d2a296e 100644 --- a/Pal98Timer/Properties/AssemblyInfo.cs +++ b/Pal98Timer/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, // 方法是按如下所示使用“*”: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.35")] -[assembly: AssemblyFileVersion("3.35")] +[assembly: AssemblyVersion("3.35.1")] +[assembly: AssemblyFileVersion("3.35.1")] diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index d43e867..37614e5 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -441,17 +441,17 @@ public override bool NeedBlockCtrlEnter() public override string GetMoreInfo() { // 在战斗中显示实时数据(功能2) - short displayHCG = MaxHCG + (IsInBattle ? CurrentBattleHCG : (short)0); - short displayXLL = MaxXLL + (IsInBattle ? CurrentBattleXLL : (short)0); - short displayLQJ = MaxLQJ + (IsInBattle ? CurrentBattleLQJ : (short)0); + int displayHCG = MaxHCG + (IsInBattle ? CurrentBattleHCG : (int)0); + int displayXLL = MaxXLL + (IsInBattle ? CurrentBattleXLL : (int)0); + int displayLQJ = MaxLQJ + (IsInBattle ? CurrentBattleLQJ : (int)0); if (IsShowSpeed) { - return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount.ToString("D3"); + return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount; } else { - return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount.ToString("D3"); + return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount; } } diff --git a/README.md b/README.md index 1ed6cf2..93e3cbf 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # 自动计时器使用说明 -**需要 .net framework 2.0 框架** +**需要 .net framework 4.0 框架** 目前支持自动计时的游戏(点击主界面右上方的设置按钮可以选择切换): 1. 仙剑98柔情版比赛专用版(补丁版本2.0.10.3、3.0.2014.628) -2. 仙剑98柔情版Steam版 -3. 仙剑2 Steam版64位 -4. 仙剑3 -5. 仙剑5前传 -6. 仙剑梦幻2.2版 -7. 古剑奇谭2 +2. 仙剑98柔情版2025新补丁版本(支持新补丁的常规版本显示) +3. 仙剑98柔情版Steam版 +4. 仙剑2 Steam版64位 +5. 仙剑3 +6. 仙剑5前传 +7. 仙剑梦幻2.2版 +8. 古剑奇谭2 ## 快捷键 考虑到部分游戏可能使用F1~F8作为快捷键,所以在3.34版本及以后,在默认情况下,F9为暂停/启动计时器,F10为重置计时器,F11为快速启用/禁止改键功能 @@ -17,6 +18,7 @@ |内核\快捷键|F6|F8|F9|F10|F11|F12| |----|----|----|----|----|----|----| |仙剑98|切换简版|暂停|暂停|重置|改键开关|-| +|仙剑98DX9|切换简版|暂停|暂停|重置|改键开关|-| |仙剑98Steam|切换简版|暂停|暂停|重置|改键开关|-| |仙剑二Steam|-|-|暂停|重置|改键开关|-| |仙剑三|-|暂停|暂停|重置|改键开关|-| @@ -30,6 +32,7 @@ |内核中文名|内核英文名| |-----|-----| |仙剑98柔情|PAL98| +|仙剑98DX9|PAL98DX9| |仙剑98官方Steam|PAL98STM| |仙剑二官方Steam|PAL2STM| |仙剑三|PAL3| @@ -92,6 +95,18 @@ ~~以上几种直播方式的效果和系统资源占用都不太一样,请选择你最合适的方式进行直播。当然,对选手的视线干扰也可以几乎没有,更多功能可以自行摸索。~~ # 更新说明 +## v3.35.1 (2026-02-01 13:19 by othercat) +为2025年新补丁进行版本适配,计时器默认使用新补丁配置 + +火,血,剑能够实时获取,注意重启之后数字依旧累加的逻辑没有改变 + +实时统计撞怪次数 + +不允许同时运行多个计时器 + +能够调整更宽的行距 + +改键器名字修正 ## v3.34.1 (2024-05-10 16:30)赛前紧急更新 取消了直播友好化功能; From 196fc501e3df58973f2d7d344b9c6a5f8c71bc14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:03:42 +0000 Subject: [PATCH 12/24] Fix main timer hour display clipping with large fonts Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index a092d9f..89fb9c8 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1042,7 +1042,7 @@ private void BuildRects() ModifyRect(ref rcBR, rcBL.X + rcBL.Width, rcBL.Y, Width - 2 * rcBL.X - rcBL.Width, rcBL.Height); ModifyRect(ref rcMoreInfo, 5, Height - 100, Width - 10, 26); - ModifyRect(ref rcMainTimer, 20, Height - 150, Width - 65, 50); + ModifyRect(ref rcMainTimer, 0, Height - 150, Width - 45, 50); ModifyRect(ref rcMainTimerMS, rcMainTimer.X + rcMainTimer.Width, rcMainTimer.Y, Width - 25 - rcMainTimer.Width, rcMainTimer.Height); ModifyRect(ref rcIsC, -1, rcMainTimer.Y + 5, 20, rcMainTimer.Height - 10); From 278259172e1a5b396db3b853363773dc923ba12f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:04:32 +0000 Subject: [PATCH 13/24] Allow DX9 to load PAL98 plugins for bottom-left display compatibility Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 37614e5..e04aa1c 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -1651,5 +1651,43 @@ public override string ForCloudLiteData() { return MT.CurrentTS.Ticks.ToString() + "," + ST.CurrentTS.Ticks.ToString() + "," + BattleLong.Ticks.ToString(); } + + // Override LoadPlugins to also load PAL98 plugins for compatibility + public override void LoadPlugins() + { + // First load PAL98DX9-specific plugins + base.LoadPlugins(); + + // Then also load PAL98 plugins for compatibility + // This allows PAL98 plugins (like bottom-left money/item display) to work with DX9 + string pluginPath = TimerPluginBase.TimerPluginPackageInfo.GetPluginDir(); + if (!System.IO.Directory.Exists(pluginPath)) return; + + System.IO.DirectoryInfo root = new System.IO.DirectoryInfo(pluginPath); + System.IO.FileInfo[] files = root.GetFiles(); + foreach (var f in files) + { + string sn = f.FullName.Replace(pluginPath, ""); + if (sn.StartsWith("PAL98.") && sn.EndsWith(".tpg")) + { + string tpsfile = f.FullName; + if (System.IO.File.Exists(tpsfile)) + { + try + { + // Use reflection to call private _loadOnePlugin method + var method = typeof(TimerCore).GetMethod("_loadOnePlugin", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + if (method != null) + { + method.Invoke(this, new object[] { tpsfile }); + } + } + catch + { } + } + } + } + } } } From 3118a13307ad6d6c8270839d95459548da35b22d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:09:56 +0000 Subject: [PATCH 14/24] Fix LoadPlugins compilation errors - make virtual and fix namespace Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/TimerCore.cs | 2 +- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pal98Timer/TimerCore.cs b/Pal98Timer/TimerCore.cs index d3829f6..2b0ad56 100644 --- a/Pal98Timer/TimerCore.cs +++ b/Pal98Timer/TimerCore.cs @@ -940,7 +940,7 @@ public string GetRStr() /// /// 加载此内核能用的所有插件 /// - public void LoadPlugins() + public virtual void LoadPlugins() { string pluginPath = TimerPluginPackageInfo.GetPluginDir(); if (!Directory.Exists(pluginPath)) return; diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index e04aa1c..8d6ef37 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -1660,7 +1660,7 @@ public override void LoadPlugins() // Then also load PAL98 plugins for compatibility // This allows PAL98 plugins (like bottom-left money/item display) to work with DX9 - string pluginPath = TimerPluginBase.TimerPluginPackageInfo.GetPluginDir(); + string pluginPath = TimerPluginPackageInfo.GetPluginDir(); if (!System.IO.Directory.Exists(pluginPath)) return; System.IO.DirectoryInfo root = new System.IO.DirectoryInfo(pluginPath); From 3d36dd33cd853108f16d9d1cceef9b214e89bb7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:21:38 +0000 Subject: [PATCH 15/24] Fix milliseconds display wrapping by increasing allocated width Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 89fb9c8..98b30ac 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1042,7 +1042,7 @@ private void BuildRects() ModifyRect(ref rcBR, rcBL.X + rcBL.Width, rcBL.Y, Width - 2 * rcBL.X - rcBL.Width, rcBL.Height); ModifyRect(ref rcMoreInfo, 5, Height - 100, Width - 10, 26); - ModifyRect(ref rcMainTimer, 0, Height - 150, Width - 45, 50); + ModifyRect(ref rcMainTimer, 0, Height - 150, Width - 85, 50); ModifyRect(ref rcMainTimerMS, rcMainTimer.X + rcMainTimer.Width, rcMainTimer.Y, Width - 25 - rcMainTimer.Width, rcMainTimer.Height); ModifyRect(ref rcIsC, -1, rcMainTimer.Y + 5, 20, rcMainTimer.Height - 10); From c07e0aee5b13473c937c6a03613574bba59a2b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:29:46 +0000 Subject: [PATCH 16/24] =?UTF-8?q?Fix=20=E7=AE=80=E7=89=88=20layout=20to=20?= =?UTF-8?q?prevent=20overlapping=20and=20text=20clipping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 98b30ac..7c3ca4a 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1041,14 +1041,31 @@ private void BuildRects() ModifyRect(ref rcBL, 5, Height - 26, GEX.GDIMulti(Width, 0.4F), 26); ModifyRect(ref rcBR, rcBL.X + rcBL.Width, rcBL.Y, Width - 2 * rcBL.X - rcBL.Width, rcBL.Height); - ModifyRect(ref rcMoreInfo, 5, Height - 100, Width - 10, 26); - ModifyRect(ref rcMainTimer, 0, Height - 150, Width - 85, 50); - ModifyRect(ref rcMainTimerMS, rcMainTimer.X + rcMainTimer.Width, rcMainTimer.Y, Width - 25 - rcMainTimer.Width, rcMainTimer.Height); - ModifyRect(ref rcIsC, -1, rcMainTimer.Y + 5, 20, rcMainTimer.Height - 10); + // For small windows (简版: 270x200), adjust layout to prevent overlapping + if (Height <= 250) + { + // Compact layout for 简版 + ModifyRect(ref rcMoreInfo, 5, Height - 75, Width - 10, 20); + ModifyRect(ref rcMainTimer, 0, Height - 120, Width - 70, 40); + ModifyRect(ref rcMainTimerMS, rcMainTimer.X + rcMainTimer.Width, rcMainTimer.Y, Width - 10 - rcMainTimer.Width, rcMainTimer.Height); + ModifyRect(ref rcIsC, -1, rcMainTimer.Y + 5, 20, rcMainTimer.Height - 10); + + ModifyRect(ref rcSubTimer, 10, Height - 140, GEX.GDIMulti(Width, 0.35F), 20); + ModifyRect(ref rcOutTimer, rcSubTimer.X + rcSubTimer.Width, rcSubTimer.Y, Width - 2 * rcSubTimer.X - rcSubTimer.Width, rcSubTimer.Height); + ModifyRect(ref rcWillClear, 10, Height - 160, Width - 20, 20); + } + else + { + // Standard layout for normal windows + ModifyRect(ref rcMoreInfo, 5, Height - 100, Width - 10, 26); + ModifyRect(ref rcMainTimer, 0, Height - 150, Width - 85, 50); + ModifyRect(ref rcMainTimerMS, rcMainTimer.X + rcMainTimer.Width, rcMainTimer.Y, Width - 25 - rcMainTimer.Width, rcMainTimer.Height); + ModifyRect(ref rcIsC, -1, rcMainTimer.Y + 5, 20, rcMainTimer.Height - 10); - ModifyRect(ref rcSubTimer, 10, Height - 170, GEX.GDIMulti(Width, 0.35F), 26); - ModifyRect(ref rcOutTimer, rcSubTimer.X + rcSubTimer.Width, rcSubTimer.Y, Width - 2 * rcSubTimer.X - rcSubTimer.Width, rcSubTimer.Height); - ModifyRect(ref rcWillClear, 10, Height - 200, Width - 20, 26); + ModifyRect(ref rcSubTimer, 10, Height - 170, GEX.GDIMulti(Width, 0.35F), 26); + ModifyRect(ref rcOutTimer, rcSubTimer.X + rcSubTimer.Width, rcSubTimer.Y, Width - 2 * rcSubTimer.X - rcSubTimer.Width, rcSubTimer.Height); + ModifyRect(ref rcWillClear, 10, Height - 200, Width - 20, 26); + } ModifyRect(ref rcDots, 0, 60, Width, 30); From d5ee0994ba1cf3ec960157a7d2d5d8c53a6285f4 Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 1 Feb 2026 14:49:42 +0800 Subject: [PATCH 17/24] Bump version to 3.35.2 and update DX9 references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated version numbers to 3.35.2 in GForm and AssemblyInfo. Changed UI text and logic to reference '98速通DX9版' and '仙剑98DX9' instead of previous names. Updated README with new version and description for 2025 patch adaptation. --- Pal98Timer/GForm.cs | 2 +- Pal98Timer/Properties/AssemblyInfo.cs | 4 ++-- "Pal98Timer/\344\273\231\345\211\22198DX9.cs" | 2 +- "Pal98Timer/\347\256\200\347\211\210.cs" | 4 ++-- README.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Pal98Timer/GForm.cs b/Pal98Timer/GForm.cs index b819ed6..d15e3df 100644 --- a/Pal98Timer/GForm.cs +++ b/Pal98Timer/GForm.cs @@ -12,7 +12,7 @@ namespace Pal98Timer { public partial class GForm : NoneBoardFormEx { - public const string CurrentVersion = "3.35.1"; + public const string CurrentVersion = "3.35.2"; public const string bgpath = @"bg.png"; private TimerCore core; private bool IsAutoLuck = false; diff --git a/Pal98Timer/Properties/AssemblyInfo.cs b/Pal98Timer/Properties/AssemblyInfo.cs index d2a296e..d5230da 100644 --- a/Pal98Timer/Properties/AssemblyInfo.cs +++ b/Pal98Timer/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值, // 方法是按如下所示使用“*”: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("3.35.1")] -[assembly: AssemblyFileVersion("3.35.1")] +[assembly: AssemblyVersion("3.35.2")] +[assembly: AssemblyFileVersion("3.35.2")] diff --git "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" index 8d6ef37..6c4f631 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -399,7 +399,7 @@ public override string GetGameVersion() { if (PID != -1) { - return "仙剑98原版 2025新补丁 " + DX9Version; + return "仙剑98原版 新补丁 " + DX9Version; } else { diff --git "a/Pal98Timer/\347\256\200\347\211\210.cs" "b/Pal98Timer/\347\256\200\347\211\210.cs" index 7964a23..85253c8 100644 --- "a/Pal98Timer/\347\256\200\347\211\210.cs" +++ "b/Pal98Timer/\347\256\200\347\211\210.cs" @@ -113,7 +113,7 @@ public override void InitUI() }; btnSwitch = form.NewMenuItem(); - btnSwitch.Text = "切换至98速通版"; + btnSwitch.Text = "切换至98速通DX9版"; btnSwitch.Click += BtnSwitch_Click; /*form.pnMid.Visible = false; @@ -135,7 +135,7 @@ public override void UnloadUI() private void BtnSwitch_Click(object sender, EventArgs e) { - LoadCore(new 仙剑98柔情(form)); + LoadCore(new 仙剑98DX9(form)); } private bool IsBegin = false; diff --git a/README.md b/README.md index 93e3cbf..ff9a0a4 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ ~~以上几种直播方式的效果和系统资源占用都不太一样,请选择你最合适的方式进行直播。当然,对选手的视线干扰也可以几乎没有,更多功能可以自行摸索。~~ # 更新说明 -## v3.35.1 (2026-02-01 13:19 by othercat) +## v3.35.2 (2026-02-01 14:37 by othercat) 为2025年新补丁进行版本适配,计时器默认使用新补丁配置 火,血,剑能够实时获取,注意重启之后数字依旧累加的逻辑没有改变 From cb04bbf771b1d9afc3ee4d389566b7d60e7e720d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:55:35 +0000 Subject: [PATCH 18/24] Fix proportional scaling of three columns in checkpoint items section Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 7c3ca4a..b66fa2c 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1088,10 +1088,15 @@ private void BuildRects_Item(bool showScroll) ModifyRect(ref rcItems, 0, 95, Width, Height - 200 - 95); ModifyRect(ref rcItemScroll, 0, 0, 0, 0); } - ModifyRect(ref rcIName, rcItems.X, 0, rcItems.Width - 170, bb.ItemHeight); - ModifyRect(ref rcIBest, rcItems.X + rcItems.Width - 170, 0, 60, bb.ItemHalfHeight); - ModifyRect(ref rcICha, rcIBest.X, 0, rcIBest.Width, bb.ItemHeight - bb.ItemHalfHeight); - ModifyRect(ref rcICur, rcItems.X + rcItems.Width - 110, 0, 110, bb.ItemHeight); + // Make all three columns proportional for proper scaling + int nameWidth = GEX.GDIMulti(rcItems.Width, 0.65F); + int bestWidth = GEX.GDIMulti(rcItems.Width, 0.12F); + int curWidth = rcItems.Width - nameWidth - bestWidth; + + ModifyRect(ref rcIName, rcItems.X, 0, nameWidth, bb.ItemHeight); + ModifyRect(ref rcIBest, rcItems.X + nameWidth, 0, bestWidth, bb.ItemHalfHeight); + ModifyRect(ref rcICha, rcIBest.X, bb.ItemHalfHeight, rcIBest.Width, bb.ItemHeight - bb.ItemHalfHeight); + ModifyRect(ref rcICur, rcItems.X + nameWidth + bestWidth, 0, curWidth, bb.ItemHeight); } public GBoardChanger CurrentEdit = null; public delegate void delOnEditCurrentChanged(GBoardChanger gbc); From 1da7a7ee906de486a92b9e24a22a14b8c82641d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:07:13 +0000 Subject: [PATCH 19/24] Implement adaptive width for name column based on text content Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index b66fa2c..d30af21 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1088,8 +1088,9 @@ private void BuildRects_Item(bool showScroll) ModifyRect(ref rcItems, 0, 95, Width, Height - 200 - 95); ModifyRect(ref rcItemScroll, 0, 0, 0, 0); } - // Make all three columns proportional for proper scaling - int nameWidth = GEX.GDIMulti(rcItems.Width, 0.65F); + + // Calculate adaptive width for name column based on actual text content + int nameWidth = CalculateOptimalNameWidth(rcItems.Width); int bestWidth = GEX.GDIMulti(rcItems.Width, 0.12F); int curWidth = rcItems.Width - nameWidth - bestWidth; @@ -1098,6 +1099,46 @@ private void BuildRects_Item(bool showScroll) ModifyRect(ref rcICha, rcIBest.X, bb.ItemHalfHeight, rcIBest.Width, bb.ItemHeight - bb.ItemHalfHeight); ModifyRect(ref rcICur, rcItems.X + nameWidth + bestWidth, 0, curWidth, bb.ItemHeight); } + + private int CalculateOptimalNameWidth(int totalWidth) + { + // If no graphics context or no items, use reasonable default (50% instead of 65%) + if (CG == null || itemList.Count == 0) + { + return GEX.GDIMulti(totalWidth, 0.50F); + } + + // Measure the widest checkpoint name + float maxTextWidth = 0; + foreach (GItem item in itemList) + { + SizeF textSize = CG.MeasureString(item.Name, bb.CPNameFont); + if (textSize.Width > maxTextWidth) + { + maxTextWidth = textSize.Width; + } + } + + // Add padding (20% extra for comfort) + int calculatedWidth = (int)(maxTextWidth * 1.2F); + + // Apply constraints: min 30%, max 55% of total width + int minWidth = GEX.GDIMulti(totalWidth, 0.30F); + int maxWidth = GEX.GDIMulti(totalWidth, 0.55F); + + if (calculatedWidth < minWidth) + { + return minWidth; + } + else if (calculatedWidth > maxWidth) + { + return maxWidth; + } + else + { + return calculatedWidth; + } + } public GBoardChanger CurrentEdit = null; public delegate void delOnEditCurrentChanged(GBoardChanger gbc); public delOnEditCurrentChanged OnEditCurrentChanged = null; From 26dee48e0f4563998bee4bb6ab6b21ecf587e2cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:16:10 +0000 Subject: [PATCH 20/24] Make all three columns adaptive based on text content Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 98 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index d30af21..2363850 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1089,10 +1089,9 @@ private void BuildRects_Item(bool showScroll) ModifyRect(ref rcItemScroll, 0, 0, 0, 0); } - // Calculate adaptive width for name column based on actual text content - int nameWidth = CalculateOptimalNameWidth(rcItems.Width); - int bestWidth = GEX.GDIMulti(rcItems.Width, 0.12F); - int curWidth = rcItems.Width - nameWidth - bestWidth; + // Calculate adaptive widths for all three columns based on actual text content + int nameWidth, bestWidth, curWidth; + CalculateOptimalColumnWidths(rcItems.Width, out nameWidth, out bestWidth, out curWidth); ModifyRect(ref rcIName, rcItems.X, 0, nameWidth, bb.ItemHeight); ModifyRect(ref rcIBest, rcItems.X + nameWidth, 0, bestWidth, bb.ItemHalfHeight); @@ -1100,43 +1099,86 @@ private void BuildRects_Item(bool showScroll) ModifyRect(ref rcICur, rcItems.X + nameWidth + bestWidth, 0, curWidth, bb.ItemHeight); } - private int CalculateOptimalNameWidth(int totalWidth) + private void CalculateOptimalColumnWidths(int totalWidth, out int nameWidth, out int bestWidth, out int curWidth) { - // If no graphics context or no items, use reasonable default (50% instead of 65%) + // If no graphics context or no items, use reasonable defaults if (CG == null || itemList.Count == 0) { - return GEX.GDIMulti(totalWidth, 0.50F); + nameWidth = GEX.GDIMulti(totalWidth, 0.50F); + bestWidth = GEX.GDIMulti(totalWidth, 0.12F); + curWidth = totalWidth - nameWidth - bestWidth; + return; } - // Measure the widest checkpoint name - float maxTextWidth = 0; + // Measure the widest text in each column + float maxNameWidth = 0; + float maxBestWidth = 0; + float maxCurWidth = 0; + foreach (GItem item in itemList) { - SizeF textSize = CG.MeasureString(item.Name, bb.CPNameFont); - if (textSize.Width > maxTextWidth) + // Measure name column + SizeF nameSize = CG.MeasureString(item.Name, bb.CPNameFont); + if (nameSize.Width > maxNameWidth) + { + maxNameWidth = nameSize.Width; + } + + // Measure best time column (format: "00:23:42" or similar) + string bestText = "00:00:00"; // Default template + if (!string.IsNullOrEmpty(item.BestStr)) + { + bestText = item.BestStr; + } + SizeF bestSize = CG.MeasureString(bestText, bb.CPBestFont); + if (bestSize.Width > maxBestWidth) { - maxTextWidth = textSize.Width; + maxBestWidth = bestSize.Width; + } + + // Measure current time column (format: "00:23:42 43" with delta) + string curText = "00:00:00 00"; // Default template + if (!string.IsNullOrEmpty(item.CurrentStr)) + { + curText = item.CurrentStr; + } + SizeF curSize = CG.MeasureString(curText, bb.CPCurFont); + if (curSize.Width > maxCurWidth) + { + maxCurWidth = curSize.Width; } } - // Add padding (20% extra for comfort) - int calculatedWidth = (int)(maxTextWidth * 1.2F); + // Add padding: 20% for names (relatively static), 30% for times (can change) + int calculatedName = (int)(maxNameWidth * 1.2F); + int calculatedBest = (int)(maxBestWidth * 1.3F); + int calculatedCur = (int)(maxCurWidth * 1.3F); - // Apply constraints: min 30%, max 55% of total width - int minWidth = GEX.GDIMulti(totalWidth, 0.30F); - int maxWidth = GEX.GDIMulti(totalWidth, 0.55F); + // Apply per-column constraints + // Name: min 25%, max 55% + int minName = GEX.GDIMulti(totalWidth, 0.25F); + int maxName = GEX.GDIMulti(totalWidth, 0.55F); + nameWidth = Math.Max(minName, Math.Min(maxName, calculatedName)); - if (calculatedWidth < minWidth) - { - return minWidth; - } - else if (calculatedWidth > maxWidth) - { - return maxWidth; - } - else - { - return calculatedWidth; + // Best: min 10%, max 25% + int minBest = GEX.GDIMulti(totalWidth, 0.10F); + int maxBest = GEX.GDIMulti(totalWidth, 0.25F); + bestWidth = Math.Max(minBest, Math.Min(maxBest, calculatedBest)); + + // Current: min 20%, max 50% + int minCur = GEX.GDIMulti(totalWidth, 0.20F); + int maxCur = GEX.GDIMulti(totalWidth, 0.50F); + curWidth = Math.Max(minCur, Math.Min(maxCur, calculatedCur)); + + // Check if total exceeds available width and scale proportionally if needed + int total = nameWidth + bestWidth + curWidth; + if (total > totalWidth) + { + // Scale all columns proportionally to fit + float scale = (float)totalWidth / (float)total; + nameWidth = (int)(nameWidth * scale); + bestWidth = (int)(bestWidth * scale); + curWidth = totalWidth - nameWidth - bestWidth; // Ensure exact fit } } public GBoardChanger CurrentEdit = null; From beca119ad6250dd1de2b02cb670a7be2ca0ccd53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:19:58 +0000 Subject: [PATCH 21/24] Fix missing BestStr and CurrentStr properties by using TimeSpan formatting methods Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 2363850..a5eb2e4 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1125,23 +1125,15 @@ private void CalculateOptimalColumnWidths(int totalWidth, out int nameWidth, out } // Measure best time column (format: "00:23:42" or similar) - string bestText = "00:00:00"; // Default template - if (!string.IsNullOrEmpty(item.BestStr)) - { - bestText = item.BestStr; - } + string bestText = TS2HHMMSS(item.Best); SizeF bestSize = CG.MeasureString(bestText, bb.CPBestFont); if (bestSize.Width > maxBestWidth) { maxBestWidth = bestSize.Width; } - // Measure current time column (format: "00:23:42 43" with delta) - string curText = "00:00:00 00"; // Default template - if (!string.IsNullOrEmpty(item.CurrentStr)) - { - curText = item.CurrentStr; - } + // Measure current time column (format: "00:23:42.67" with milliseconds) + string curText = TS2HHMMSSFF(item.Cur); SizeF curSize = CG.MeasureString(curText, bb.CPCurFont); if (curSize.Width > maxCurWidth) { From c12a0f8f1a76dbab4e41f36bb09eb117720c8aa0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:26:26 +0000 Subject: [PATCH 22/24] Fix top section stretching and center-align main timer Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index a5eb2e4..87eb5b5 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1035,9 +1035,11 @@ private void ModifyRect(ref Rectangle rect, int x, int y, int w, int h) } private void BuildRects() { - ModifyRect(ref rcTitle, 5, 5, Width - 100, 26); - ModifyRect(ref rcGameVersion, 5, 31, GEX.GDIMulti(Width, 0.7F), 26); - ModifyRect(ref rcVersion, rcGameVersion.X + rcGameVersion.Width, rcGameVersion.Y, Width - 2 * rcGameVersion.X - rcGameVersion.Width, rcGameVersion.Height); + // Top section: Make all three columns stretch proportionally with window width + // Title takes 35% on the left, GameVersion takes 40%, Version fills the rest + ModifyRect(ref rcTitle, 5, 5, GEX.GDIMulti(Width, 0.35F), 26); + ModifyRect(ref rcGameVersion, rcTitle.X + rcTitle.Width, 31, GEX.GDIMulti(Width, 0.40F), 26); + ModifyRect(ref rcVersion, rcGameVersion.X + rcGameVersion.Width, rcGameVersion.Y, Width - rcGameVersion.X - rcGameVersion.Width - 5, rcGameVersion.Height); ModifyRect(ref rcBL, 5, Height - 26, GEX.GDIMulti(Width, 0.4F), 26); ModifyRect(ref rcBR, rcBL.X + rcBL.Width, rcBL.Y, Width - 2 * rcBL.X - rcBL.Width, rcBL.Height); @@ -2428,11 +2430,11 @@ private void DrawMainTimer(Graphics g, delUpdateRect ur = null) } if (isInCheck) { - GEX.DrawText(g, "*" + TS2HHMMSS(MainTimer), bb.MainTimerFont, bb.MainTimerFill, bb.MainTimerBorder, rcMainTimer, GLayout.sfFC); + GEX.DrawText(g, "*" + TS2HHMMSS(MainTimer), bb.MainTimerFont, bb.MainTimerFill, bb.MainTimerBorder, rcMainTimer, GLayout.sfCC); } else { - GEX.DrawText(g, TS2HHMMSS(MainTimer), bb.MainTimerFont, bb.MainTimerFill, bb.MainTimerBorder, rcMainTimer, GLayout.sfFC); + GEX.DrawText(g, TS2HHMMSS(MainTimer), bb.MainTimerFont, bb.MainTimerFill, bb.MainTimerBorder, rcMainTimer, GLayout.sfCC); } if (!isSizeChanged && !isBGChanged && !isBBChanged) { From acc0cdb713dbba275b431f2e61dfc69f15a9662b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:32:15 +0000 Subject: [PATCH 23/24] Fix top section layout - correctly handle two-row structure Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 87eb5b5..38f5732 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -1035,10 +1035,11 @@ private void ModifyRect(ref Rectangle rect, int x, int y, int w, int h) } private void BuildRects() { - // Top section: Make all three columns stretch proportionally with window width - // Title takes 35% on the left, GameVersion takes 40%, Version fills the rest - ModifyRect(ref rcTitle, 5, 5, GEX.GDIMulti(Width, 0.35F), 26); - ModifyRect(ref rcGameVersion, rcTitle.X + rcTitle.Width, 31, GEX.GDIMulti(Width, 0.40F), 26); + // Top section: Two rows that stretch to fill full width + // Row 1: Title (full width) + ModifyRect(ref rcTitle, 5, 5, Width - 10, 26); + // Row 2: GameVersion (left 70%) and Version (right 30%) + ModifyRect(ref rcGameVersion, 5, 31, GEX.GDIMulti(Width, 0.70F), 26); ModifyRect(ref rcVersion, rcGameVersion.X + rcGameVersion.Width, rcGameVersion.Y, Width - rcGameVersion.X - rcGameVersion.Width - 5, rcGameVersion.Height); ModifyRect(ref rcBL, 5, Height - 26, GEX.GDIMulti(Width, 0.4F), 26); ModifyRect(ref rcBR, rcBL.X + rcBL.Width, rcBL.Y, Width - 2 * rcBL.X - rcBL.Width, rcBL.Height); From adb4deed3a4cc48fd1a8326fb6a963bce5cd8d05 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 07:39:03 +0000 Subject: [PATCH 24/24] Adjust checkpoint column text alignments: left, center, right Co-authored-by: othercat <509048+othercat@users.noreply.github.com> --- Pal98Timer/GEX.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index 38f5732..5c89ce1 100644 --- a/Pal98Timer/GEX.cs +++ b/Pal98Timer/GEX.cs @@ -857,8 +857,8 @@ public bool Draw(Graphics g, bool isForceDrawAll, GBoard bb, Rectangle rcName, R g.FillRectangles(bb.CPItemBG, new Rectangle[] { rcName, rcBest }); } - GEX.DrawText(g, _name, bb.CPNameFont, bb.CPNameFill, bb.CPNameBorder, rcName, GLayout.sfCC); - GEX.DrawText(g, TS2HHMMSS(_best), bb.CPBestFont, bb.CPBestFill, bb.CPBestBorder, rcBest, GLayout.sfFC); + GEX.DrawText(g, _name, bb.CPNameFont, bb.CPNameFill, bb.CPNameBorder, rcName, GLayout.sfNC); + GEX.DrawText(g, TS2HHMMSS(_best), bb.CPBestFont, bb.CPBestFill, bb.CPBestBorder, rcBest, GLayout.sfCC); if (!isForceDrawAll) { ur?.Invoke(rcName); @@ -888,21 +888,21 @@ public bool Draw(Graphics g, bool isForceDrawAll, GBoard bb, Rectangle rcName, R //快 fill = bb.CPGoodFill; border = bb.CPGoodBorder; - GEX.DrawText(g, _cha.Hours == 0 ? ("-" + TS2MSS(_cha)) : ("-" + TS2HMMSS(_cha)), bb.CPChaFont, fill, border, rcCha, GLayout.sfFC); + GEX.DrawText(g, _cha.Hours == 0 ? ("-" + TS2MSS(_cha)) : ("-" + TS2HMMSS(_cha)), bb.CPChaFont, fill, border, rcCha, GLayout.sfCC); } else if (_cha.Ticks > 10000000) { //慢 fill = bb.CPBadFill; border = bb.CPBadBorder; - GEX.DrawText(g, _cha.Hours == 0 ? ("+" + TS2MSS(_cha)) : ("+" + TS2HMMSS(_cha)), bb.CPChaFont, fill, border, rcCha, GLayout.sfFC); + GEX.DrawText(g, _cha.Hours == 0 ? ("+" + TS2MSS(_cha)) : ("+" + TS2HMMSS(_cha)), bb.CPChaFont, fill, border, rcCha, GLayout.sfCC); } else { //同 fill = bb.CPSameFill; border = bb.CPSameBorder; - GEX.DrawText(g, "0:00", bb.CPChaFont, fill, border, rcCha, GLayout.sfFC); + GEX.DrawText(g, "0:00", bb.CPChaFont, fill, border, rcCha, GLayout.sfCC); } GEX.DrawText(g, TS2HHMMSSFF(_cur), bb.CPCurFont, fill, border, rcCur, GLayout.sfFC);