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..2776575 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" }); @@ -47,7 +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原版 2025新补丁:").Link(c["othercat"]) .Line() .Text("仙剑98 Steam:").Link(c["寒泠"]) .Line() diff --git a/Pal98Timer/GEX.cs b/Pal98Timer/GEX.cs index a092d9f..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); @@ -1035,20 +1035,40 @@ 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: 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); - ModifyRect(ref rcMoreInfo, 5, Height - 100, Width - 10, 26); - ModifyRect(ref rcMainTimer, 20, Height - 150, Width - 65, 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); @@ -1071,10 +1091,90 @@ 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); + + // 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); + 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 void CalculateOptimalColumnWidths(int totalWidth, out int nameWidth, out int bestWidth, out int curWidth) + { + // If no graphics context or no items, use reasonable defaults + if (CG == null || itemList.Count == 0) + { + nameWidth = GEX.GDIMulti(totalWidth, 0.50F); + bestWidth = GEX.GDIMulti(totalWidth, 0.12F); + curWidth = totalWidth - nameWidth - bestWidth; + return; + } + + // Measure the widest text in each column + float maxNameWidth = 0; + float maxBestWidth = 0; + float maxCurWidth = 0; + + foreach (GItem item in itemList) + { + // 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 = 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.67" with milliseconds) + string curText = TS2HHMMSSFF(item.Cur); + SizeF curSize = CG.MeasureString(curText, bb.CPCurFont); + if (curSize.Width > maxCurWidth) + { + maxCurWidth = curSize.Width; + } + } + + // 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 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)); + + // 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; public delegate void delOnEditCurrentChanged(GBoardChanger gbc); @@ -2331,11 +2431,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) { diff --git a/Pal98Timer/GForm.cs b/Pal98Timer/GForm.cs index 973d6c7..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.34.1"; + public const string CurrentVersion = "3.35.2"; 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); @@ -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/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/Program.cs b/Pal98Timer/Program.cs index f0aa65b..c22706d 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("自动计时器已经在运行中!", "提示", 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/Properties/AssemblyInfo.cs b/Pal98Timer/Properties/AssemblyInfo.cs index c870a75..d5230da 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.2")] +[assembly: AssemblyFileVersion("3.35.2")] 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 434d9ea..6c4f631 100644 --- "a/Pal98Timer/\344\273\231\345\211\22198DX9.cs" +++ "b/Pal98Timer/\344\273\231\345\211\22198DX9.cs" @@ -68,6 +68,23 @@ 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 = ""; + + private DateTime? InitialDetectionTime = null; // 首次检测到游戏的时间 + 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"; @@ -207,13 +224,182 @@ 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() { if (PID != -1) { - return "新补丁 " + DX9Version; + return "仙剑98原版 新补丁 " + DX9Version; } else { @@ -240,150 +426,123 @@ public override void Reset() MaxTLF = 0; BattleLong = new TimeSpan(0); ST.Reset(); + WillAppendNamedBattle = ""; NamedBattleRes = new List(); + InitialDetectionTime = null; + HasConfirmedDX9 = false; + TotalMonsterCount = 0; // 重置撞怪计数器 } - protected override void Checking() + public override bool NeedBlockCtrlEnter() { - if (GetPalHandle()) + return false; + } + + public override string GetMoreInfo() + { + // 在战斗中显示实时数据(功能2) + int displayHCG = MaxHCG + (IsInBattle ? CurrentBattleHCG : (int)0); + int displayXLL = MaxXLL + (IsInBattle ? CurrentBattleXLL : (int)0); + int displayLQJ = MaxLQJ + (IsInBattle ? CurrentBattleLQJ : (int)0); + + if (IsShowSpeed) { - if (cryerror != "") - { - Error(cryerror); - cryerror = ""; - } + return MoveSpeed.ToString("F2") + " " + "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount; } else { - if (cryerror != "") - { - Error(cryerror); - cryerror = ""; - } - return; + return "蜂" + MaxFC + " 蜜" + MaxFM + " 火" + displayHCG + " 血" + displayXLL + " 夜" + MaxYXY + " 剑" + displayLQJ + ((MaxTLF > 0) ? (" 土" + MaxTLF) : "") + ((MaxQTJ > 0) ? (" 甲" + MaxQTJ) : "") + " 怪" + TotalMonsterCount; } + } - if (PID == -1) return; - - base.Checking(); + public override string GetSmallWatch() + { + return BattleLong.TotalSeconds.ToString("F2") + "s"; } - private bool GetPalHandle() + public override string GetSecondWatch() { - Process[] res = Process.GetProcessesByName("Pal"); - - // 功能2: 过滤已退出的进程 - if (res.Length > 1) + if (ST.CurrentTSOnly.Ticks == 0) { - var aliveProcesses = res.Where(p => { - try { return !p.HasExited; } - catch { return false; } - }).ToArray(); - - if (aliveProcesses.Length > 1) - { - if (!HasAlertMutiPal) - { - cryerror = "检测到多个Pal.exe进程,请关闭其他的,只保留一个!"; - HasAlertMutiPal = true; - } - return false; - } - res = aliveProcesses; + return ""; } + return ST.ToString(); + } - HasAlertMutiPal = false; - if (res.Length > 0) + public override TimeSpan GetMainWatch() + { + return MT.CurrentTS; + } + + public override bool IsMainWatchStar() + { + return IsInUnCheat; + } + + public override string GetPointEnd() + { + return "预计通关 " + GetWillClearStr(); + } + + public override string GetPointSpan() + { + if (PointSpanName == "") return "--"; + return PointSpanName + " " + GetPointSpanStr(); + } + + public override string GetAAction() + { + if (WillAppendNamedBattle == "") { - if (PID == -1) - { - IntPtr tempHandle = res[0].MainWindowHandle; - StringBuilder sb = new StringBuilder(256); - User32.GetWindowText(tempHandle, sb, sb.Capacity); - string windowTitle = sb.ToString(); - - // 功能1: 窗口标题识别 - if (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("DX9移植版")) - { - // 提取版本号 - int versionStartIndex = windowTitle.IndexOf("(v"); - if (versionStartIndex != -1) - { - int versionEndIndex = windowTitle.IndexOf(")", versionStartIndex); - if (versionEndIndex != -1) - { - DX9Version = windowTitle.Substring(versionStartIndex + 2, versionEndIndex - versionStartIndex - 2); - } - } - - PalProcess = res[0]; - GameWindowHandle = res[0].MainWindowHandle; - PID = PalProcess.Id; - PalHandle = new IntPtr(Kernel32.OpenProcess(0x1F0FFF, false, PID)); - CalcPalMD5(); - return true; - } - else - { - cryerror = "请使用仙剑98 DX9移植版!"; - return false; - } - } - else - { - if (PID == res[0].Id) - { - if (GMD5 == "none") - { - CalcPalMD5(); - } - return true; - } - else - { - PalHandle = IntPtr.Zero; - GameWindowHandle = IntPtr.Zero; - PalProcess = null; - PID = -1; - GMD5 = "none"; - DX9Version = "未知"; - return false; - } - } + return ""; } else { - PalHandle = IntPtr.Zero; - GameWindowHandle = IntPtr.Zero; - PalProcess = null; - PID = -1; - GMD5 = "none"; - DX9Version = "未知"; - return false; + NamedBattleRes.Add(WillAppendNamedBattle); + string res = WillAppendNamedBattle; + WillAppendNamedBattle = ""; + return res; } } - private void CalcPalMD5() + public override string GetCriticalError() { - try + if (cryerror == "") { - string dllmd5 = GetFileMD5(GetGameFilePath("Pal.dll")); - string datamd5 = GetFileMD5(GetGameFilePath("DATA.MKF")); - string sssmd5 = GetFileMD5(GetGameFilePath("SSS.MKF")); - string vb40032md5 = GetFileMD5(GetGameFilePath("VB40032.dll")); - GMD5 = dllmd5 + "_" + datamd5 + "_" + sssmd5 + "_" + vb40032md5; + return ""; } - catch + else { - GMD5 = "none"; + string tmp = cryerror; + cryerror = ""; + return tmp; } } - private string GetGameFilePath(string fn) + 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] = fn; + spli[spli.Length - 1] = ""; palpath = ""; foreach (string s in spli) { @@ -393,7 +552,1142 @@ private string GetGameFilePath(string fn) { palpath = palpath.Substring(0, palpath.Length - 1); } - return palpath; + 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; + 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()) + { + CopyRPGIfHas(); + + JudgePause(); + try + { + FlushGameObject(); + } + catch (Exception ex) + { + } + + + try + { + 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 + { + _HasGameStart = false; + MT.Stop(); + + if (_IsFirstStarted) + { + ST.Start(); + } + } + + PreData(); + } + + 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() + { + Process[] res = Process.GetProcessesByName("Pal"); + + // 功能2: 过滤已退出的进程 + if (res.Length > 1) + { + var aliveProcesses = res.Where(p => { + try { return !p.HasExited; } + catch { return false; } + }).ToArray(); + + if (aliveProcesses.Length > 1) + { + if (!HasAlertMutiPal) + { + //cryerror = "检测到多个Pal.exe进程,请关闭其他的,只保留一个!"; + HasAlertMutiPal = true; + } + return false; + } + res = aliveProcesses; + } + + HasAlertMutiPal = false; + if (res.Length > 0) + { + 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("新补丁")) || + (windowTitle.Contains("仙剑奇侠传") && windowTitle.Contains("(v")) || + (windowTitle.Contains("仙剑") && windowTitle.Contains("DX9")); + + // 检查是否是基础游戏标题(PAL.DLL还未修改标题,或VB4初始窗口) + bool isBaseGameTitle = windowTitle.Contains("仙剑奇侠传") || + windowTitle.StartsWith("PAL98") || + windowTitle.StartsWith("Pal98") || + windowTitle.Equals("sdf", StringComparison.OrdinalIgnoreCase); // VB4初始窗口 + + if (hasDX9Title) + { + // 找到DX9标题,提取版本号并连接 + int versionStartIndex = windowTitle.IndexOf("(v"); + if (versionStartIndex != -1) + { + int versionEndIndex = windowTitle.IndexOf(")", versionStartIndex); + if (versionEndIndex != -1) + { + 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; + 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; + } + } + else + { + 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(); + } + return true; + } + else + { + PalHandle = IntPtr.Zero; + GameWindowHandle = IntPtr.Zero; + PalProcess = null; + PID = -1; + GMD5 = "none"; + DX9Version = "未知"; + InitialDetectionTime = null; + HasConfirmedDX9 = false; + return false; + } + } + } + else + { + PalHandle = IntPtr.Zero; + GameWindowHandle = IntPtr.Zero; + PalProcess = null; + PID = -1; + GMD5 = "none"; + DX9Version = "未知"; + InitialDetectionTime = null; + HasConfirmedDX9 = false; + return false; + } + } + + private void CalcPalMD5() + { + try + { + string dllmd5 = GetFileMD5(GetGameFilePath("Pal.dll")); + string datamd5 = GetFileMD5(GetGameFilePath("DATA.MKF")); + string sssmd5 = GetFileMD5(GetGameFilePath("SSS.MKF")); + string vb40032md5 = GetFileMD5(GetGameFilePath("VB40032.dll")); + GMD5 = dllmd5 + "_" + datamd5 + "_" + sssmd5 + "_" + vb40032md5; + } + catch + { + GMD5 = "none"; + } + } + + private string GetGameFilePath(string fn) + { + string palpath = PalProcess.MainModule.FileName; + string[] spli = palpath.Split('\\'); + spli[spli.Length - 1] = fn; + palpath = ""; + foreach (string s in spli) + { + palpath += s + "\\"; + } + if (palpath != "") + { + palpath = palpath.Substring(0, palpath.Length - 1); + } + 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(); + TotalMonsterCount++; // 撞怪计数器增加 + + // 重置战斗中实时显示的临时变量(功能2) + CurrentBattleHCG = 0; + CurrentBattleXLL = 0; + CurrentBattleLQJ = 0; + + if (CurrentStep <= 5) + { + //战斗前记录下个数 + biw.Insert(0x73, GameObj.GetItemCount(0x73));//蜂 + 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));//夜行衣 + + biw.Insert(0x47, GameObj.GetItemCount(0x47));//土灵符 + biw.Insert(0xD5, GameObj.GetItemCount(0xD5));//青铜甲 + CurrentNamedBattle = GameObj.GetNamedBattle(); + } + + private void Battling() + { + BattleLong = DateTime.Now - InBattleTime; + biw.SetCount(GameObj); + + // 实时更新火、血、剑的数量(功能2) + CurrentBattleHCG = biw.GettedCount(0x8F); // 火虫草 + CurrentBattleXLL = biw.GettedCount(0xA2); // 血玲珑 + CurrentBattleLQJ = biw.GettedCount(0xB8); // 龙泉剑 + } + + 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 已在Battling中实时更新,这里累加最终值 + MaxHCG += biw.GettedCount(0x8F); + } + // MaxLQJ 和 MaxXLL 已在Battling中实时更新,这里累加最终值 + 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; + exdata["TotalMonsterCount"] = TotalMonsterCount; // 保存撞怪总数 + + 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(); + } + + // 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 = 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 + { } + } + } + } } } } 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 1ed6cf2..ff9a0a4 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.2 (2026-02-01 14:37 by othercat) +为2025年新补丁进行版本适配,计时器默认使用新补丁配置 + +火,血,剑能够实时获取,注意重启之后数字依旧累加的逻辑没有改变 + +实时统计撞怪次数 + +不允许同时运行多个计时器 + +能够调整更宽的行距 + +改键器名字修正 ## v3.34.1 (2024-05-10 16:30)赛前紧急更新 取消了直播友好化功能;