cheatengine 5.8.26 → 5.8.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,13 +25,13 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.js <--Named Pipe--> ce_mcp_bridge.lua (CE)
25
25
  dofile([[D:\path\to\ce_mcp_bridge.lua]])
26
26
  ```
27
27
 
28
- 3. **Configure MCP** (`.kiro/settings/mcp.json`):
28
+ 3. **Configure MCP** :
29
29
  ```json
30
30
  {
31
31
  "mcpServers": {
32
32
  "cheat-engine": {
33
33
  "command": "npx",
34
- "args": ["--yes", "cheatengine"]
34
+ "args": ["-y", "cheatengine@latest"]
35
35
  }
36
36
  }
37
37
  }
@@ -157,17 +157,19 @@ Write a value to memory.
157
157
 
158
158
  ### Scanning & Search
159
159
 
160
- #### `ce_aob_scan(aob_string, module?, protection?, max_results?)`
160
+ #### `ce_aob_scan(aob_string, module?, protection?, start?, stop?, max_results?)`
161
161
  Scan memory for Array of Bytes pattern. Supports `??` wildcards.
162
162
 
163
163
  **Parameters:**
164
164
  - `aob_string` (string, required): Pattern like `"48 89 5C 24 ?? 48 83 EC 20"`
165
165
  - `module` (string, optional): Limit scan to module (e.g. `"game.exe"`)
166
166
  - `protection` (string, optional): Memory protection flags (default: `"-C+X"`)
167
+ - `start` (string, optional): Start address if module not specified
168
+ - `stop` (string, optional): Stop address if module not specified
167
169
  - `max_results` (integer, optional): Maximum results (default: 100)
168
170
 
169
171
  #### `ce_value_scan(value, type, module?, protection?)`
170
- Scan for a specific value. Useful for pointer tracing. **One-shot scan - for iterative scanning use Scan Sessions.**
172
+ Scan for a specific value. Useful for pointer tracing. Auto-aligns scan based on type for performance. **One-shot scan - for iterative scanning use Scan Sessions.**
171
173
 
172
174
  **Parameters:**
173
175
  - `value` (string, required): Value to search (e.g. `"0x255D5E758"` or `"12345"`)
@@ -182,7 +184,7 @@ Scan for a specific value. Useful for pointer tracing. **One-shot scan - for ite
182
184
  Implements CE's core "First Scan → Next Scan" workflow with session management. Sessions auto-expire after 5 minutes of inactivity.
183
185
 
184
186
  #### `ce_scan_new(value, type, module?, protection?)`
185
- Start a new scan session.
187
+ Start a new scan session. Auto-aligns scan based on type for performance (4-byte for dword/float, 8-byte for qword/double).
186
188
 
187
189
  #### `ce_scan_next(session_id, value, scan_type?, value2?)`
188
190
  Continue scanning (filter) an existing session.
package/README_CN.md CHANGED
@@ -7,7 +7,7 @@ MCP 桥接器,让 AI 助手直接控制 Cheat Engine 进行游戏修改和逆
7
7
  ## 架构
8
8
 
9
9
  ```
10
- AI <--MCP/JSON-RPC--> ce_mcp_server.py <--命名管道--> ce_mcp_bridge.lua (CE)
10
+ AI <--MCP/JSON-RPC--> ce_mcp_server.js <--命名管道--> ce_mcp_bridge.lua (CE)
11
11
 
12
12
  后台自动重连
13
13
  ```
@@ -16,7 +16,7 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.py <--命名管道--> ce_mcp_bridge.lua (CE)
16
16
 
17
17
  ### 快速开始 (NPX) - 推荐
18
18
 
19
- 1. **安装依赖**: `pip install pywin32`
19
+ 1. **前提条件**: Node.js 14+ (无需其他依赖项)
20
20
 
21
21
  2. **在 CE 中加载** (二选一):
22
22
  - **自动加载**: 将 `ce_mcp_bridge.lua` 复制到 CE 的 `autorun` 文件夹 (如 `D:\Cheat Engine\autorun\`)
@@ -25,13 +25,13 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.py <--命名管道--> ce_mcp_bridge.lua (CE)
25
25
  dofile([[D:\path\to\ce_mcp_bridge.lua]])
26
26
  ```
27
27
 
28
- 3. **配置 MCP** (`.kiro/settings/mcp.json`):
28
+ 3. **配置 MCP** :
29
29
  ```json
30
30
  {
31
31
  "mcpServers": {
32
32
  "cheat-engine": {
33
33
  "command": "npx",
34
- "args": ["--yes", "cheatengine"]
34
+ "args": ["-y", "cheatengine@latest"]
35
35
  }
36
36
  }
37
37
  }
@@ -45,8 +45,8 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.py <--命名管道--> ce_mcp_bridge.lua (CE)
45
45
  {
46
46
  "mcpServers": {
47
47
  "cheat-engine": {
48
- "command": "python",
49
- "args": ["D:/path/to/ce_mcp/ce_mcp_server.py"]
48
+ "command": "node",
49
+ "args": ["D:/path/to/ce_mcp/ce_mcp_server.js"]
50
50
  }
51
51
  }
52
52
  }
@@ -157,17 +157,19 @@ Hook 名称现在会验证以防止 AA 脚本注入:
157
157
 
158
158
  ### 扫描与搜索
159
159
 
160
- #### `ce_aob_scan(aob_string, module?, protection?, max_results?)`
160
+ #### `ce_aob_scan(aob_string, module?, protection?, start?, stop?, max_results?)`
161
161
  扫描内存中的字节数组模式。支持 `??` 通配符。
162
162
 
163
163
  **参数:**
164
164
  - `aob_string` (string, 必需): 模式如 `"48 89 5C 24 ?? 48 83 EC 20"`
165
165
  - `module` (string, 可选): 限制扫描到模块 (如 `"game.exe"`)
166
166
  - `protection` (string, 可选): 内存保护标志 (默认: `"-C+X"`)
167
+ - `start` (string, 可选): 起始地址(未指定 module 时使用)
168
+ - `stop` (string, 可选): 结束地址(未指定 module 时使用)
167
169
  - `max_results` (integer, 可选): 最大结果数 (默认: 100)
168
170
 
169
171
  #### `ce_value_scan(value, type, module?, protection?)`
170
- 扫描特定值。用于指针追踪。**一次性扫描 - 迭代扫描请使用扫描会话。**
172
+ 扫描特定值。用于指针追踪。根据类型自动对齐扫描以优化性能。**一次性扫描 - 迭代扫描请使用扫描会话。**
171
173
 
172
174
  **参数:**
173
175
  - `value` (string, 必需): 要搜索的值 (如 `"0x255D5E758"` 或 `"12345"`)
@@ -182,7 +184,7 @@ Hook 名称现在会验证以防止 AA 脚本注入:
182
184
  实现 CE 核心的 "首次扫描 → 再次扫描" 工作流,带会话管理。会话在 5 分钟不活动后自动过期。
183
185
 
184
186
  #### `ce_scan_new(value, type, module?, protection?)`
185
- 开始新的扫描会话。
187
+ 开始新的扫描会话。根据类型自动对齐扫描以优化性能(dword/float 用 4 字节对齐,qword/double 用 8 字节对齐)。
186
188
 
187
189
  #### `ce_scan_next(session_id, value, scan_type?, value2?)`
188
190
  继续扫描(过滤)现有会话。
@@ -383,47 +385,47 @@ Hook 函数以拦截调用并捕获参数。
383
385
 
384
386
  ## 推荐工作流
385
387
 
386
- ### 指针追踪
387
- ```json
388
- // 自动 (首选)
389
- {"name": "ce_find_pointer_path", "arguments": {"address": "0x255D5E758", "user_prompted": true}}
390
- // 返回: base_address, offsets, ce_pointer_notation
391
-
392
- // 手动 (自动失败时)
393
- // 1. 查找访问该地址的代码
394
- {"name": "ce_find_what_accesses", "arguments": {"address": "0x255D5E758", "user_prompted": true}}
395
- // 2. 从结果获取寄存器值 (如 RBX=0x255D5E658)
396
- // 3. 搜索存储该值的指针
397
- {"name": "ce_value_scan", "arguments": {"value": "0x255D5E658", "type": "qword"}}
398
- // 4. 重复直到找到 game.exe+offset
399
- ```
400
-
401
- ### 函数分析
402
- ```json
403
- // 1. 查找函数边界
404
- {"name": "ce_find_function_boundaries", "arguments": {"address": "0x14587EDB0"}}
405
-
406
- // 2. 跟踪执行
407
- {"name": "ce_break_and_trace", "arguments": {"address": "0x14587EDB0", "max_steps": 100}}
408
-
409
- // 3. 生成特征码用于更新
410
- {"name": "ce_generate_signature", "arguments": {"address": "0x14587EDB0"}}
411
- ```
412
-
413
- ### 逆向未知代码
414
- ```json
415
- // 1. 反汇编
416
- {"name": "ce_disassemble", "arguments": {"address": "0x14587EDB0", "count": 20}}
417
-
418
- // 2. 符号跟踪理解逻辑
419
- {"name": "ce_symbolic_trace", "arguments": {"address": "0x14587EDB0", "initial_state": {"rcx": "this"}}}
420
-
421
- // 3. 为复杂函数构建 CFG
422
- {"name": "ce_build_cfg", "arguments": {"address": "0x14587EDB0"}}
423
-
424
- // 4. 检测模式
425
- {"name": "ce_detect_patterns", "arguments": {"address": "0x14587EDB0"}}
426
- ```
388
+ ### 指针追踪
389
+ ```json
390
+ // 自动 (首选)
391
+ {"name": "ce_find_pointer_path", "arguments": {"address": "0x255D5E758", "user_prompted": true}}
392
+ // 返回: base_address, offsets, ce_pointer_notation
393
+
394
+ // 手动 (自动失败时)
395
+ // 1. 查找访问该地址的代码
396
+ {"name": "ce_find_what_accesses", "arguments": {"address": "0x255D5E758", "user_prompted": true}}
397
+ // 2. 从结果获取寄存器值 (如 RBX=0x255D5E658)
398
+ // 3. 搜索存储该值的指针
399
+ {"name": "ce_value_scan", "arguments": {"value": "0x255D5E658", "type": "qword"}}
400
+ // 4. 重复直到找到 game.exe+offset
401
+ ```
402
+
403
+ ### 函数分析
404
+ ```json
405
+ // 1. 查找函数边界
406
+ {"name": "ce_find_function_boundaries", "arguments": {"address": "0x14587EDB0"}}
407
+
408
+ // 2. 跟踪执行
409
+ {"name": "ce_break_and_trace", "arguments": {"address": "0x14587EDB0", "max_steps": 100}}
410
+
411
+ // 3. 生成特征码用于更新
412
+ {"name": "ce_generate_signature", "arguments": {"address": "0x14587EDB0"}}
413
+ ```
414
+
415
+ ### 逆向未知代码
416
+ ```json
417
+ // 1. 反汇编
418
+ {"name": "ce_disassemble", "arguments": {"address": "0x14587EDB0", "count": 20}}
419
+
420
+ // 2. 符号跟踪理解逻辑
421
+ {"name": "ce_symbolic_trace", "arguments": {"address": "0x14587EDB0", "initial_state": {"rcx": "this"}}}
422
+
423
+ // 3. 为复杂函数构建 CFG
424
+ {"name": "ce_build_cfg", "arguments": {"address": "0x14587EDB0"}}
425
+
426
+ // 4. 检测模式
427
+ {"name": "ce_detect_patterns", "arguments": {"address": "0x14587EDB0"}}
428
+ ```
427
429
 
428
430
  ---
429
431
 
package/ce_mcp_bridge.lua CHANGED
@@ -26,14 +26,14 @@ local pairs = pairs
26
26
  local ipairs = ipairs
27
27
  local os_clock = os.clock
28
28
 
29
- -- ============ Config ============
30
- local Config = {
31
- -- Pipe name: supports environment variable override for anti-detection
32
- -- Set CE_MCP_PIPE_NAME environment variable to customize
33
- PIPE_NAME = os.getenv("CE_MCP_PIPE_NAME") or "ce_mcp_bridge",
34
- -- Authentication token: optional security layer for pipe communication
35
- -- Set CE_MCP_AUTH_TOKEN environment variable to enable authentication
36
- AUTH_TOKEN = os.getenv("CE_MCP_AUTH_TOKEN") or nil,
29
+ -- ============ Config ============
30
+ local Config = {
31
+ -- Pipe name: supports environment variable override for anti-detection
32
+ -- Set CE_MCP_PIPE_NAME environment variable to customize
33
+ PIPE_NAME = os.getenv("CE_MCP_PIPE_NAME") or "ce_mcp_bridge",
34
+ -- Authentication token: optional security layer for pipe communication
35
+ -- Set CE_MCP_AUTH_TOKEN environment variable to enable authentication
36
+ AUTH_TOKEN = os.getenv("CE_MCP_AUTH_TOKEN") or nil,
37
37
  DEBUG_MODE = false,
38
38
  MAX_MESSAGE_SIZE = 10 * 1024 * 1024,
39
39
  PIPE_BUFFER_SIZE = 1024 * 1024,
@@ -1458,16 +1458,16 @@ local function getDebuggerStatus()
1458
1458
  return status
1459
1459
  end
1460
1460
 
1461
- Handlers.ping = function()
1462
- return {
1463
- status = "ok",
1464
- timestamp = os.time(),
1465
- process = process or "N/A",
1466
- pid = getOpenedProcessID() or 0,
1467
- connections = Context.connectionCount,
1468
- debugger = getDebuggerStatus()
1469
- }
1470
- end
1461
+ Handlers.ping = function()
1462
+ return {
1463
+ status = "ok",
1464
+ timestamp = os.time(),
1465
+ process = process or "N/A",
1466
+ pid = getOpenedProcessID() or 0,
1467
+ connections = Context.connectionCount,
1468
+ debugger = getDebuggerStatus()
1469
+ }
1470
+ end
1471
1471
 
1472
1472
  Handlers.get_process_info = function()
1473
1473
  -- Refresh symbol table using ModuleManager
@@ -1556,16 +1556,16 @@ Handlers.stats = function(p)
1556
1556
  })
1557
1557
  end
1558
1558
 
1559
- return {
1560
- uptime = metrics.uptime,
1561
- commands = metrics.commands,
1562
- summary = metrics.summary,
1563
- cache = metrics.cache,
1564
- logging = logStats,
1565
- scanSessions = {
1566
- active = scanSessionCount,
1567
- maxAllowed = Config.MAX_SCAN_SESSIONS,
1568
- sessions = scanSessions
1559
+ return {
1560
+ uptime = metrics.uptime,
1561
+ commands = metrics.commands,
1562
+ summary = metrics.summary,
1563
+ cache = metrics.cache,
1564
+ logging = logStats,
1565
+ scanSessions = {
1566
+ active = scanSessionCount,
1567
+ maxAllowed = Config.MAX_SCAN_SESSIONS,
1568
+ sessions = scanSessions
1569
1569
  },
1570
1570
  connections = Context.connectionCount,
1571
1571
  debugger = getDebuggerStatus()
@@ -1707,25 +1707,41 @@ Handlers.aob_scan = function(p)
1707
1707
  end
1708
1708
 
1709
1709
  -- Execute scan
1710
- local scanResult = nil
1711
- local success = pcall(function()
1712
- if startAddr and stopAddr then
1713
- scanResult = AOBScan(cleanAob, protection, startAddr, stopAddr)
1714
- else
1715
- scanResult = AOBScan(cleanAob, protection)
1716
- end
1710
+ -- Use createMemScan to properly support start/stop range (AOBScan function doesn't support range args correctly)
1711
+ local memscan = createMemScan()
1712
+ local foundList = createFoundList(memscan)
1713
+ local scanOk = false
1714
+
1715
+ local vtByteArray = vtByteArray or 7
1716
+ local rtRounded = rtRounded or 0
1717
+ local fsmNotAligned = fsmNotAligned or 0
1718
+ local soExactValue = soExactValue or 1
1719
+
1720
+ pcall(function()
1721
+ memscan.firstScan(
1722
+ soExactValue, vtByteArray, rtRounded,
1723
+ cleanAob, "",
1724
+ startAddr or 0, stopAddr or 0x7FFFFFFFFFFFFFFF,
1725
+ protection, fsmNotAligned, "",
1726
+ true, false, false, false
1727
+ )
1728
+ memscan.waitTillDone()
1729
+ scanOk = true
1717
1730
  end)
1718
1731
 
1719
- if not success or not scanResult then
1720
- return { count = 0, results = {}, message = "No results found" }
1732
+ if not scanOk then
1733
+ pcall(function() memscan.destroy() end)
1734
+ return { count = 0, results = {}, error = "AOB Scan failed" }
1721
1735
  end
1722
1736
 
1737
+ foundList.initialize()
1738
+ local totalCount = foundList.Count or 0
1739
+
1723
1740
  local results = {}
1724
- local totalCount = scanResult.Count or 0
1725
1741
  local count = math_min(totalCount, maxResults)
1726
1742
 
1727
1743
  for i = 0, count - 1 do
1728
- local addr = scanResult[i]
1744
+ local addr = foundList.Address[i]
1729
1745
  if addr then
1730
1746
  if type(addr) == "string" then
1731
1747
  table_insert(results, addr)
@@ -1735,7 +1751,8 @@ Handlers.aob_scan = function(p)
1735
1751
  end
1736
1752
  end
1737
1753
 
1738
- scanResult.destroy()
1754
+ foundList.deinitialize()
1755
+ memscan.destroy()
1739
1756
 
1740
1757
  return {
1741
1758
  count = totalCount,
@@ -1807,8 +1824,14 @@ Handlers.value_scan = function(p)
1807
1824
 
1808
1825
  -- Execute scan
1809
1826
  local scanOk = pcall(function()
1827
+ -- Determine alignment
1828
+ local alignType = fsmAligned
1829
+ local alignParam = "4"
1830
+ if vt == vtQword or vt == vtDouble then alignParam = "8" end
1831
+ if vt == vtByte then alignType = fsmNotAligned end
1832
+
1810
1833
  memscan.firstScan(soExactValue, vt, rtRounded, scanValue, "",
1811
- startAddr, stopAddr, protection, fsmNotAligned, "",
1834
+ startAddr, stopAddr, protection, alignType, alignParam,
1812
1835
  isHex, false, false, false)
1813
1836
  memscan.waitTillDone()
1814
1837
  end)
@@ -1951,8 +1974,14 @@ Handlers.scan_new = function(p)
1951
1974
 
1952
1975
  -- Execute first scan
1953
1976
  local scanOk = pcall(function()
1977
+ -- Determine alignment
1978
+ local alignType = fsmAligned
1979
+ local alignParam = "4"
1980
+ if vt == vtQword or vt == vtDouble then alignParam = "8" end
1981
+ if vt == vtByte then alignType = fsmNotAligned end
1982
+
1954
1983
  memscan.firstScan(soExactValue, vt, rtRounded, scanValue, "",
1955
- startAddr, stopAddr, protection, fsmNotAligned, "",
1984
+ startAddr, stopAddr, protection, alignType, alignParam,
1956
1985
  isHex, false, false, false)
1957
1986
  memscan.waitTillDone()
1958
1987
  end)
@@ -4762,55 +4791,75 @@ Handlers.find_call_references = function(p)
4762
4791
  for _, pattern in ipairs(patterns) do
4763
4792
  if #callers >= limit then break end
4764
4793
 
4765
- local scanResult = AOBScan(pattern, "+X", segStart, segEnd)
4766
- if scanResult and scanResult.Count > 0 then
4767
- totalMatches = totalMatches + scanResult.Count
4768
-
4769
- for i = 0, scanResult.Count - 1 do
4770
- if #callers >= limit then break end
4771
-
4772
- local callAddrStr = scanResult[i]
4773
- local callAddr = tonumber(callAddrStr, 16)
4794
+ -- Use MemScan for range-limited AOB scan
4795
+ local ms = createMemScan()
4796
+ local fl = createFoundList(ms)
4797
+ local scanOk = false
4798
+
4799
+ pcall(function()
4800
+ -- 0=soExactValue, 7=vtByteArray, 0=rtRounded
4801
+ -- fsmNotAligned=0, ""=alignParam
4802
+ -- true=hex, false=notBinary, false=unicode, false=caseSensitive
4803
+ ms.firstScan(0, 7, 0, pattern, "", segStart, segEnd, "+X",
4804
+ 0, "", true, false, false, false)
4805
+ ms.waitTillDone()
4806
+ scanOk = true
4807
+ end)
4774
4808
 
4775
- if callAddr then
4776
- -- Verify: read actual rel32 and check target
4777
- local relOffset = readInteger(callAddr + 1)
4778
- if relOffset then
4779
- if relOffset > 0x7FFFFFFF then
4780
- relOffset = relOffset - 0x100000000
4781
- end
4782
- local target = callAddr + 5 + relOffset
4783
-
4784
- if target == funcAddr then
4785
- local disasm = ""
4786
- local symbol = nil
4787
- pcall(function()
4788
- disasm = disassemble(callAddr) or "???"
4789
- symbol = getNameFromAddress(callAddr, true)
4790
- end)
4791
-
4792
- -- Avoid duplicates
4793
- local isDup = false
4794
- for _, c in ipairs(callers) do
4795
- if c.address == Utils.formatHex(callAddr) then
4796
- isDup = true
4797
- break
4798
- end
4809
+ if scanOk then
4810
+ fl.initialize()
4811
+ local count = fl.Count or 0
4812
+ if count > 0 then
4813
+ totalMatches = totalMatches + count
4814
+
4815
+ for i = 0, count - 1 do
4816
+ if #callers >= limit then break end
4817
+
4818
+ local callAddrStr = fl.Address[i]
4819
+ -- Address can be string or number depending on CE version
4820
+ local callAddr = type(callAddrStr) == "string" and tonumber(callAddrStr, 16) or callAddrStr
4821
+
4822
+ if callAddr then
4823
+ -- Verify: read actual rel32 and check target
4824
+ local relOffset = readInteger(callAddr + 1)
4825
+ if relOffset then
4826
+ if relOffset > 0x7FFFFFFF then
4827
+ relOffset = relOffset - 0x100000000
4799
4828
  end
4829
+ local target = callAddr + 5 + relOffset
4830
+
4831
+ if target == funcAddr then
4832
+ local disasm = ""
4833
+ local symbol = nil
4834
+ pcall(function()
4835
+ disasm = disassemble(callAddr) or "???"
4836
+ symbol = getNameFromAddress(callAddr, true)
4837
+ end)
4838
+
4839
+ -- Avoid duplicates
4840
+ local isDup = false
4841
+ for _, c in ipairs(callers) do
4842
+ if c.address == Utils.formatHex(callAddr) then
4843
+ isDup = true
4844
+ break
4845
+ end
4846
+ end
4800
4847
 
4801
- if not isDup then
4802
- table_insert(callers, {
4803
- address = Utils.formatHex(callAddr),
4804
- instruction = disasm,
4805
- symbol = symbol
4806
- })
4848
+ if not isDup then
4849
+ table_insert(callers, {
4850
+ address = Utils.formatHex(callAddr),
4851
+ instruction = disasm,
4852
+ symbol = symbol
4853
+ })
4854
+ end
4807
4855
  end
4808
4856
  end
4809
4857
  end
4810
4858
  end
4811
4859
  end
4812
- scanResult.destroy()
4860
+ fl.deinitialize()
4813
4861
  end
4862
+ ms.destroy()
4814
4863
  end
4815
4864
  end
4816
4865
 
@@ -7485,162 +7534,162 @@ function Pipe.executeRequest(req)
7485
7534
  end
7486
7535
  end
7487
7536
 
7488
- -- Helper function to send error response without breaking connection
7489
- local function sendErrorResponse(errorMsg)
7490
- local resp = { error = errorMsg }
7491
- local respStr = JSON.encode(resp)
7492
- local respLen = #respStr
7493
-
7494
- pcall(function()
7495
- Context.pipeServer.lock()
7496
- pcall(function()
7497
- Context.pipeServer.writeDword(respLen)
7498
- Context.pipeServer.writeString(respStr, false)
7499
- end)
7500
- Context.pipeServer.unlock()
7501
- end)
7502
- end
7503
-
7504
- function Pipe.workerLoop()
7505
- Utils.debugPrint("Worker started")
7506
- local consecutiveErrors = 0
7507
-
7508
- while Context.serverRunning do
7509
- local acceptOk, acceptErr = pcall(function()
7510
- Context.pipeServer.acceptConnection()
7511
- end)
7512
-
7513
- if not acceptOk then
7514
- consecutiveErrors = consecutiveErrors + 1
7515
- if consecutiveErrors > Config.MAX_CONSECUTIVE_ERRORS then
7516
- Utils.debugPrint("Too many consecutive errors, restarting pipe...")
7517
- pcall(function() Context.pipeServer.destroy() end)
7518
- Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7519
- consecutiveErrors = 0
7520
- end
7521
- if Context.serverRunning then
7522
- sleep(Config.HEARTBEAT_INTERVAL)
7523
- end
7524
- else
7525
- consecutiveErrors = 0
7526
- Context.connectionCount = Context.connectionCount + 1
7527
- Context.lastActivityTime = os_clock() -- Heartbeat: record connection time
7528
- Utils.debugPrint("Client connected (#" .. Context.connectionCount .. ")")
7529
-
7530
- local sessionErrors = 0
7531
-
7532
- while Context.serverRunning and Context.pipeServer and Context.pipeServer.Valid do
7533
- Utils.periodicCleanup()
7534
-
7535
- -- Read size
7536
- local ok, size = false, nil
7537
- pcall(function()
7538
- Context.pipeServer.lock()
7539
- ok, size = pcall(Context.pipeServer.readDword)
7540
- Context.pipeServer.unlock()
7541
- end)
7542
-
7543
- if not ok or not size or size == 0 then
7544
- sessionErrors = sessionErrors + 1
7545
- if sessionErrors >= Config.MAX_SESSION_ERRORS then
7546
- Utils.debugPrint("Too many session errors, disconnecting client")
7547
- -- Send error before disconnecting
7548
- sendErrorResponse("Too many consecutive read errors")
7549
- break
7550
- end
7551
- sleep(10)
7552
- goto continue
7553
- end
7554
-
7555
- if size >= Config.MAX_MESSAGE_SIZE then
7556
- Utils.debugPrint("Message too large: " .. size)
7557
- sendErrorResponse("Message too large: " .. size .. " bytes (max: " .. Config.MAX_MESSAGE_SIZE .. ")")
7558
- break
7559
- end
7560
-
7561
- sessionErrors = 0
7562
-
7563
- -- Read payload
7564
- local ok2, payload = false, nil
7565
- pcall(function()
7566
- Context.pipeServer.lock()
7567
- ok2, payload = pcall(Context.pipeServer.readString, size)
7568
- Context.pipeServer.unlock()
7569
- end)
7570
-
7571
- if ok2 and payload and #payload == size then
7572
- local decodeOk, req = pcall(JSON.decode, payload)
7573
- if not decodeOk or not req then
7574
- Utils.debugPrint("Failed to decode JSON payload")
7575
- sendErrorResponse("Invalid JSON payload")
7576
- goto continue
7577
- end
7578
-
7579
- local resp = nil
7580
-
7581
- -- Use optimized request execution (sync on demand)
7582
- resp = Pipe.executeRequest(req)
7583
-
7584
- local respStr = JSON.encode(resp)
7585
- local respLen = #respStr
7586
-
7587
- if respLen > Config.MAX_MESSAGE_SIZE then
7588
- respStr = JSON.encode({ error = "Response too large" })
7589
- respLen = #respStr
7590
- end
7591
-
7592
- local writeOk = false
7593
- pcall(function()
7594
- Context.pipeServer.lock()
7595
- writeOk = pcall(function()
7596
- Context.pipeServer.writeDword(respLen)
7597
- Context.pipeServer.writeString(respStr, false)
7598
- end)
7599
- Context.pipeServer.unlock()
7600
- end)
7601
-
7602
- if writeOk then
7603
- Context.lastActivityTime = os_clock() -- Heartbeat: update on successful communication
7604
- else
7605
- Utils.debugPrint("Failed to write response")
7606
- break
7607
- end
7608
- else
7609
- Utils.debugPrint("Failed to read payload: expected " .. size .. " bytes")
7610
- sendErrorResponse("Failed to read payload: expected " .. size .. " bytes, got " .. tostring(#(payload or "")))
7611
- -- Don't break, allow client to retry
7612
- sleep(50)
7613
- end
7614
-
7615
- ::continue::
7616
- end
7617
-
7618
- pcall(function()
7619
- if Context.pipeServer and Context.pipeServer.Valid then
7620
- Context.pipeServer.disconnect()
7621
- end
7622
- end)
7623
- Context.lastActivityTime = 0 -- Heartbeat: reset on disconnect
7624
- Utils.debugPrint("Client disconnected, recreating pipe for next connection...")
7625
-
7626
- -- Recreate pipe instance for next client (Windows named pipes are single-client)
7627
- pcall(function()
7628
- if Context.pipeServer then
7629
- Context.pipeServer.destroy()
7630
- end
7631
- end)
7632
- Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7633
-
7634
- if not Context.pipeServer or not Context.pipeServer.Valid then
7635
- Utils.debugPrint("Failed to recreate pipe, restarting worker loop...")
7636
- sleep(100)
7637
- else
7638
- Utils.debugPrint("Pipe recreated, ready for next connection")
7639
- end
7640
- end
7641
- end
7642
- Utils.debugPrint("Worker stopped")
7643
- end
7537
+ -- Helper function to send error response without breaking connection
7538
+ local function sendErrorResponse(errorMsg)
7539
+ local resp = { error = errorMsg }
7540
+ local respStr = JSON.encode(resp)
7541
+ local respLen = #respStr
7542
+
7543
+ pcall(function()
7544
+ Context.pipeServer.lock()
7545
+ pcall(function()
7546
+ Context.pipeServer.writeDword(respLen)
7547
+ Context.pipeServer.writeString(respStr, false)
7548
+ end)
7549
+ Context.pipeServer.unlock()
7550
+ end)
7551
+ end
7552
+
7553
+ function Pipe.workerLoop()
7554
+ Utils.debugPrint("Worker started")
7555
+ local consecutiveErrors = 0
7556
+
7557
+ while Context.serverRunning do
7558
+ local acceptOk, acceptErr = pcall(function()
7559
+ Context.pipeServer.acceptConnection()
7560
+ end)
7561
+
7562
+ if not acceptOk then
7563
+ consecutiveErrors = consecutiveErrors + 1
7564
+ if consecutiveErrors > Config.MAX_CONSECUTIVE_ERRORS then
7565
+ Utils.debugPrint("Too many consecutive errors, restarting pipe...")
7566
+ pcall(function() Context.pipeServer.destroy() end)
7567
+ Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7568
+ consecutiveErrors = 0
7569
+ end
7570
+ if Context.serverRunning then
7571
+ sleep(Config.HEARTBEAT_INTERVAL)
7572
+ end
7573
+ else
7574
+ consecutiveErrors = 0
7575
+ Context.connectionCount = Context.connectionCount + 1
7576
+ Context.lastActivityTime = os_clock() -- Heartbeat: record connection time
7577
+ Utils.debugPrint("Client connected (#" .. Context.connectionCount .. ")")
7578
+
7579
+ local sessionErrors = 0
7580
+
7581
+ while Context.serverRunning and Context.pipeServer and Context.pipeServer.Valid do
7582
+ Utils.periodicCleanup()
7583
+
7584
+ -- Read size
7585
+ local ok, size = false, nil
7586
+ pcall(function()
7587
+ Context.pipeServer.lock()
7588
+ ok, size = pcall(Context.pipeServer.readDword)
7589
+ Context.pipeServer.unlock()
7590
+ end)
7591
+
7592
+ if not ok or not size or size == 0 then
7593
+ sessionErrors = sessionErrors + 1
7594
+ if sessionErrors >= Config.MAX_SESSION_ERRORS then
7595
+ Utils.debugPrint("Too many session errors, disconnecting client")
7596
+ -- Send error before disconnecting
7597
+ sendErrorResponse("Too many consecutive read errors")
7598
+ break
7599
+ end
7600
+ sleep(10)
7601
+ goto continue
7602
+ end
7603
+
7604
+ if size >= Config.MAX_MESSAGE_SIZE then
7605
+ Utils.debugPrint("Message too large: " .. size)
7606
+ sendErrorResponse("Message too large: " .. size .. " bytes (max: " .. Config.MAX_MESSAGE_SIZE .. ")")
7607
+ break
7608
+ end
7609
+
7610
+ sessionErrors = 0
7611
+
7612
+ -- Read payload
7613
+ local ok2, payload = false, nil
7614
+ pcall(function()
7615
+ Context.pipeServer.lock()
7616
+ ok2, payload = pcall(Context.pipeServer.readString, size)
7617
+ Context.pipeServer.unlock()
7618
+ end)
7619
+
7620
+ if ok2 and payload and #payload == size then
7621
+ local decodeOk, req = pcall(JSON.decode, payload)
7622
+ if not decodeOk or not req then
7623
+ Utils.debugPrint("Failed to decode JSON payload")
7624
+ sendErrorResponse("Invalid JSON payload")
7625
+ goto continue
7626
+ end
7627
+
7628
+ local resp = nil
7629
+
7630
+ -- Use optimized request execution (sync on demand)
7631
+ resp = Pipe.executeRequest(req)
7632
+
7633
+ local respStr = JSON.encode(resp)
7634
+ local respLen = #respStr
7635
+
7636
+ if respLen > Config.MAX_MESSAGE_SIZE then
7637
+ respStr = JSON.encode({ error = "Response too large" })
7638
+ respLen = #respStr
7639
+ end
7640
+
7641
+ local writeOk = false
7642
+ pcall(function()
7643
+ Context.pipeServer.lock()
7644
+ writeOk = pcall(function()
7645
+ Context.pipeServer.writeDword(respLen)
7646
+ Context.pipeServer.writeString(respStr, false)
7647
+ end)
7648
+ Context.pipeServer.unlock()
7649
+ end)
7650
+
7651
+ if writeOk then
7652
+ Context.lastActivityTime = os_clock() -- Heartbeat: update on successful communication
7653
+ else
7654
+ Utils.debugPrint("Failed to write response")
7655
+ break
7656
+ end
7657
+ else
7658
+ Utils.debugPrint("Failed to read payload: expected " .. size .. " bytes")
7659
+ sendErrorResponse("Failed to read payload: expected " .. size .. " bytes, got " .. tostring(#(payload or "")))
7660
+ -- Don't break, allow client to retry
7661
+ sleep(50)
7662
+ end
7663
+
7664
+ ::continue::
7665
+ end
7666
+
7667
+ pcall(function()
7668
+ if Context.pipeServer and Context.pipeServer.Valid then
7669
+ Context.pipeServer.disconnect()
7670
+ end
7671
+ end)
7672
+ Context.lastActivityTime = 0 -- Heartbeat: reset on disconnect
7673
+ Utils.debugPrint("Client disconnected, recreating pipe for next connection...")
7674
+
7675
+ -- Recreate pipe instance for next client (Windows named pipes are single-client)
7676
+ pcall(function()
7677
+ if Context.pipeServer then
7678
+ Context.pipeServer.destroy()
7679
+ end
7680
+ end)
7681
+ Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7682
+
7683
+ if not Context.pipeServer or not Context.pipeServer.Valid then
7684
+ Utils.debugPrint("Failed to recreate pipe, restarting worker loop...")
7685
+ sleep(100)
7686
+ else
7687
+ Utils.debugPrint("Pipe recreated, ready for next connection")
7688
+ end
7689
+ end
7690
+ end
7691
+ Utils.debugPrint("Worker stopped")
7692
+ end
7644
7693
 
7645
7694
  -- ============ Server Lifecycle ============
7646
7695
  local Server = {}
@@ -7669,13 +7718,13 @@ function Server.stop()
7669
7718
  Utils.debugPrint("Server stopped (handled " .. Context.connectionCount .. " connections)")
7670
7719
  end
7671
7720
 
7672
- function Server.start()
7673
- Server.stop() -- This calls cleanupZombieState()
7674
-
7675
- -- Additional cleanup for fresh start
7676
- Utils.debugPrint("Starting MCP Bridge")
7677
-
7678
- Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7721
+ function Server.start()
7722
+ Server.stop() -- This calls cleanupZombieState()
7723
+
7724
+ -- Additional cleanup for fresh start
7725
+ Utils.debugPrint("Starting MCP Bridge")
7726
+
7727
+ Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7679
7728
  if not Context.pipeServer or not Context.pipeServer.Valid then
7680
7729
  print("[CE-MCP] Failed to create pipe: " .. Config.PIPE_NAME)
7681
7730
  return false
@@ -7708,14 +7757,14 @@ function Server.stats()
7708
7757
  bpCount = bpCount + 1
7709
7758
  end
7710
7759
 
7711
- return {
7712
- running = Context.serverRunning,
7713
- connections = Context.connectionCount,
7714
- cached_symbols = symbolCacheCount,
7715
- cached_modules = moduleCacheCount,
7716
- active_breakpoints = bpCount
7717
- }
7718
- end
7760
+ return {
7761
+ running = Context.serverRunning,
7762
+ connections = Context.connectionCount,
7763
+ cached_symbols = symbolCacheCount,
7764
+ cached_modules = moduleCacheCount,
7765
+ active_breakpoints = bpCount
7766
+ }
7767
+ end
7719
7768
 
7720
7769
  -- ============ Global API ============
7721
7770
  -- Cleanup old instance
@@ -7734,18 +7783,18 @@ if CE_MCP_STATUS_LABEL then
7734
7783
  end)
7735
7784
  end
7736
7785
 
7737
- CE_MCP = {
7738
- start = Server.start,
7739
- stop = Server.stop,
7740
- stats = Server.stats,
7741
- -- Logger API
7742
- Logger = Logger,
7743
- LogLevel = LogLevel,
7744
- -- Expose for debugging
7745
- _config = Config,
7746
- _context = Context,
7747
- _handlers = Handlers,
7748
- }
7786
+ CE_MCP = {
7787
+ start = Server.start,
7788
+ stop = Server.stop,
7789
+ stats = Server.stats,
7790
+ -- Logger API
7791
+ Logger = Logger,
7792
+ LogLevel = LogLevel,
7793
+ -- Expose for debugging
7794
+ _config = Config,
7795
+ _context = Context,
7796
+ _handlers = Handlers,
7797
+ }
7749
7798
 
7750
7799
  CE_MCP_BRIDGE_INSTANCE = CE_MCP
7751
7800
 
@@ -7770,15 +7819,26 @@ function StatusLabel.create()
7770
7819
  label.Cursor = crHandPoint
7771
7820
 
7772
7821
  if commentBtn then
7773
- label.Parent = commentBtn.Parent
7774
- -- Anchor to CommentButton's left side
7775
- label.AnchorSideRight.Control = commentBtn
7776
- label.AnchorSideRight.Side = asrLeft
7777
- label.AnchorSideTop.Control = commentBtn
7778
- label.AnchorSideTop.Side = asrTop
7779
- label.Anchors = "[akTop,akRight]"
7780
- label.BorderSpacing.Right = 5
7781
- label.BorderSpacing.Top = 2
7822
+ -- Use pcall to safely set properties, in case of unexpected parent/control issues
7823
+ local ok = pcall(function()
7824
+ label.Parent = commentBtn.Parent
7825
+ -- Anchor to CommentButton's left side
7826
+ label.AnchorSideRight.Control = commentBtn
7827
+ label.AnchorSideRight.Side = asrLeft
7828
+ label.AnchorSideTop.Control = commentBtn
7829
+ label.AnchorSideTop.Side = asrTop
7830
+ label.Anchors = "[akTop,akRight]"
7831
+ label.BorderSpacing.Right = 5
7832
+ label.BorderSpacing.Top = 2
7833
+ end)
7834
+
7835
+ if not ok then
7836
+ -- Fallback if anchoring failed
7837
+ label.Parent = mainForm
7838
+ label.Anchors = "[akRight, akTop]"
7839
+ label.Left = mainForm.ClientWidth - 120
7840
+ label.Top = 5
7841
+ end
7782
7842
  else
7783
7843
  -- Fallback: top right
7784
7844
  label.Parent = mainForm
@@ -7787,15 +7847,15 @@ function StatusLabel.create()
7787
7847
  label.Top = 5
7788
7848
  end
7789
7849
 
7790
- -- Click to show stats
7791
- label.OnClick = function()
7792
- local stats = Server.stats()
7793
- local connected = Context.lastActivityTime > 0 and
7794
- (os_clock() - Context.lastActivityTime) < Config.HEARTBEAT_TIMEOUT
7795
- showMessage(string_format("CE MCP\nStatus: %s\nConnections: %d",
7796
- connected and "Connected" or "Waiting",
7797
- stats.connections or 0))
7798
- end
7850
+ -- Click to show stats
7851
+ label.OnClick = function()
7852
+ local stats = Server.stats()
7853
+ local connected = Context.lastActivityTime > 0 and
7854
+ (os_clock() - Context.lastActivityTime) < Config.HEARTBEAT_TIMEOUT
7855
+ showMessage(string_format("CE MCP\nStatus: %s\nConnections: %d",
7856
+ connected and "Connected" or "Waiting",
7857
+ stats.connections or 0))
7858
+ end
7799
7859
 
7800
7860
  StatusLabel.label = label
7801
7861
  StatusLabel.updateColor(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cheatengine",
3
- "version": "5.8.26",
3
+ "version": "5.8.28",
4
4
  "description": "Cheat Engine MCP Server - AI-assisted reverse engineering bridge",
5
5
  "main": "ce_mcp_server.js",
6
6
  "bin": {
@@ -221,8 +221,8 @@ class ToolRegistry {
221
221
  new ToolParam('aob_string', 'string', "e.g. '48 89 5C 24 ?? 48 83 EC 20'", true),
222
222
  new ToolParam('module', 'string', "Limit scan to specific module for better performance (e.g. 'game.exe', 'UnityPlayer.dll')"),
223
223
  new ToolParam('protection', 'string', 'Flags like -C+X', false, '-C+X'),
224
- new ToolParam('start', 'string', 'Start address (optional, use module instead for better performance)'),
225
- new ToolParam('stop', 'string', 'Stop address (optional)'),
224
+ new ToolParam('start', 'string', 'Start address (optional). If module is not specified, this defines the start of the scan range.'),
225
+ new ToolParam('stop', 'string', 'Stop address (optional). If module is not specified, this defines the end of the scan range.'),
226
226
  new ToolParam('max_results', 'integer', 'Maximum results', false, 100),
227
227
  ]
228
228
  ));
@@ -233,6 +233,7 @@ class ToolRegistry {
233
233
  'USE WHEN: After ce_find_what_accesses returns a register value, search for that value to find pointer storage. ' +
234
234
  "Example: RBX=0x12345678 accessed your address -> scan for 0x12345678 (qword) -> find where pointer is stored. " +
235
235
  "NOT FOR: Finding game values like health/gold (use ce_scan_new for value hunting). " +
236
+ 'Auto-aligns scan based on type for performance (4-byte for dword/float, 8-byte for qword/double). ' +
236
237
  'Returns: {count, results: [{address, symbol, isStatic}], value_searched, type, module}. ' +
237
238
  'TIP: isStatic=true means the address is in a module (potential static base found!).',
238
239
  'value_scan',
@@ -254,7 +255,7 @@ class ToolRegistry {
254
255
  '[VALUE HUNTING] Start scan session to find unknown addresses by observing value changes. ' +
255
256
  "USE WHEN: You don't know the address but can observe changes (health 100->95). " +
256
257
  "WORKFLOW: ce_scan_new -> change value in game -> ce_scan_next('decreased') -> repeat until few results. " +
257
- 'NOT FOR: Pointer tracing (use ce_value_scan). NOT FOR: Code patterns (use ce_aob_scan). ' +
258
+ 'Auto-aligns scan based on type for performance (4-byte for dword/float, 8-byte for qword/double). ' +
258
259
  'Returns: {session_id, count}. Use ce_scan_next to filter, ce_scan_results to get addresses.',
259
260
  'scan_new',
260
261
  ToolCategory.SCANNING,