cheatengine 5.8.28 → 5.8.30

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
@@ -139,12 +139,16 @@ Read a single memory value.
139
139
  - `type` (string, required): `byte`, `word`, `dword`, `qword`, `float`, `double`, `string`, `bytes`
140
140
  - `size` (integer, optional): Size for string/bytes type (default: 100)
141
141
 
142
+ **Returns:** `{value}`
143
+
142
144
  #### `ce_read_memory_batch(requests)`
143
145
  Read multiple addresses in one call. **Always prefer this over multiple ce_read_memory calls.**
144
146
 
145
147
  **Parameters:**
146
148
  - `requests` (array, required): Array of `{address, type, id?, size?}`
147
149
 
150
+ **Returns:** `{results: {id_or_address: {value, error?}}}`
151
+
148
152
  #### `ce_write_memory(address, type, value)`
149
153
  Write a value to memory.
150
154
 
@@ -153,6 +157,8 @@ Write a value to memory.
153
157
  - `type` (string, required): Value type
154
158
  - `value` (string, required): Value to write
155
159
 
160
+ **Returns:** `{success, address}`
161
+
156
162
  ---
157
163
 
158
164
  ### Scanning & Search
@@ -208,6 +214,8 @@ List all active scan sessions.
208
214
  #### `ce_enum_modules`
209
215
  List all loaded modules (DLLs).
210
216
 
217
+ **Returns:** `{count, modules: [{name, address, size, path, source}], used_fallback}`
218
+
211
219
  ---
212
220
 
213
221
  ### Symbols & Addresses
@@ -250,6 +258,8 @@ Disassemble instructions.
250
258
  #### `ce_get_instruction_info(address)`
251
259
  Get detailed info about a single instruction.
252
260
 
261
+ **Returns:** `{address, opcode, params, bytes, bytesStr, size, isCall, isJump, isRet, isConditionalJump, parameterValue}`
262
+
253
263
  #### `ce_analyze_code(address, count?)`
254
264
  Static analysis of code block (calls, jumps, refs).
255
265
 
@@ -315,6 +325,8 @@ Detect function start and end by analyzing prologue/epilogue patterns.
315
325
  #### `ce_generate_signature(address)`
316
326
  Generate unique AOB signature for an address. Useful for game updates.
317
327
 
328
+ **Returns:** `{address, signature, offset_from_start, byte_count, usage_hint}`
329
+
318
330
  ---
319
331
 
320
332
  ### Advanced Analysis
@@ -346,9 +358,16 @@ Lightweight symbolic execution. Interprets instruction semantics without executi
346
358
  - `count` (integer, optional): Instructions to trace (default: 30)
347
359
  - `initial_state` (object, optional): Initial register symbols, e.g. `{"rcx": "this_ptr", "rdx": "arg1"}`
348
360
 
349
- #### `ce_call_function(address, args?, return_type?, timeout?)`
361
+ #### `ce_call_function(address, args?, call_method?, return_type?, timeout?)`
350
362
  Call a function in the target process. **WARNING: Executes real code!**
351
363
 
364
+ **Parameters:**
365
+ - `address` (string, required): Target function address
366
+ - `args` (array, optional): Up to 4 arguments (integers or address expressions)
367
+ - `call_method` (integer, optional): Calling method for `executeCodeEx` (e.g. `0`). If omitted, bridge auto-fallbacks for compatibility
368
+ - `return_type` (string, optional): `byte`, `word`, `dword`, `qword`, `float`, `double`, `pointer` (default: `qword`)
369
+ - `timeout` (integer, optional): Timeout in milliseconds (default: `5000`)
370
+
352
371
  ---
353
372
 
354
373
  ### Function Hooking
package/README_CN.md CHANGED
@@ -131,27 +131,33 @@ Hook 名称现在会验证以防止 AA 脚本注入:
131
131
 
132
132
  ### 内存读写
133
133
 
134
- #### `ce_read_memory(address, type, size?)`
135
- 读取单个内存值。
134
+ #### `ce_read_memory(address, type, size?)`
135
+ 读取单个内存值。
136
136
 
137
137
  **参数:**
138
138
  - `address` (string, 必需): 地址表达式 (如 `"game.exe+0x1234"`, `"0x140001000"`)
139
- - `type` (string, 必需): `byte`, `word`, `dword`, `qword`, `float`, `double`, `string`, `bytes`
140
- - `size` (integer, 可选): string/bytes 类型的大小 (默认: 100)
139
+ - `type` (string, 必需): `byte`, `word`, `dword`, `qword`, `float`, `double`, `string`, `bytes`
140
+ - `size` (integer, 可选): string/bytes 类型的大小 (默认: 100)
141
+
142
+ **返回:** `{value}`
141
143
 
142
- #### `ce_read_memory_batch(requests)`
143
- 一次调用读取多个地址。**始终优先使用此方法而非多次调用 ce_read_memory。**
144
+ #### `ce_read_memory_batch(requests)`
145
+ 一次调用读取多个地址。**始终优先使用此方法而非多次调用 ce_read_memory。**
144
146
 
145
147
  **参数:**
146
- - `requests` (array, 必需): `{address, type, id?, size?}` 数组
148
+ - `requests` (array, 必需): `{address, type, id?, size?}` 数组
149
+
150
+ **返回:** `{results: {id_or_address: {value, error?}}}`
147
151
 
148
- #### `ce_write_memory(address, type, value)`
149
- 向内存写入值。
152
+ #### `ce_write_memory(address, type, value)`
153
+ 向内存写入值。
150
154
 
151
155
  **参数:**
152
- - `address` (string, 必需): 地址表达式
153
- - `type` (string, 必需): 值类型
154
- - `value` (string, 必需): 要写入的值
156
+ - `address` (string, 必需): 地址表达式
157
+ - `type` (string, 必需): 值类型
158
+ - `value` (string, 必需): 要写入的值
159
+
160
+ **返回:** `{success, address}`
155
161
 
156
162
  ---
157
163
 
@@ -205,8 +211,10 @@ Hook 名称现在会验证以防止 AA 脚本注入:
205
211
  #### `ce_scan_list`
206
212
  列出所有活动的扫描会话。
207
213
 
208
- #### `ce_enum_modules`
209
- 列出所有已加载的模块 (DLL)。
214
+ #### `ce_enum_modules`
215
+ 列出所有已加载的模块 (DLL)。
216
+
217
+ **返回:** `{count, modules: [{name, address, size, path, source}], used_fallback}`
210
218
 
211
219
  ---
212
220
 
@@ -247,8 +255,10 @@ Hook 名称现在会验证以防止 AA 脚本注入:
247
255
  - `count` (integer, 可选): 指令数量 (默认: 10)
248
256
  - `direction` (string, 可选): `"forward"` 或 `"backward"` (默认: forward)
249
257
 
250
- #### `ce_get_instruction_info(address)`
251
- 获取单条指令的详细信息。
258
+ #### `ce_get_instruction_info(address)`
259
+ 获取单条指令的详细信息。
260
+
261
+ **返回:** `{address, opcode, params, bytes, bytesStr, size, isCall, isJump, isRet, isConditionalJump, parameterValue}`
252
262
 
253
263
  #### `ce_analyze_code(address, count?)`
254
264
  代码块的静态分析 (调用、跳转、引用)。
@@ -312,8 +322,10 @@ Hook 名称现在会验证以防止 AA 脚本注入:
312
322
  #### `ce_find_function_boundaries(address, max_search?)`
313
323
  通过分析序言/尾声模式检测函数起止。
314
324
 
315
- #### `ce_generate_signature(address)`
316
- 为地址生成唯一的 AOB 特征码。用于游戏更新。
325
+ #### `ce_generate_signature(address)`
326
+ 为地址生成唯一的 AOB 特征码。用于游戏更新。
327
+
328
+ **返回:** `{address, signature, offset_from_start, byte_count, usage_hint}`
317
329
 
318
330
  ---
319
331
 
@@ -346,8 +358,15 @@ Hook 名称现在会验证以防止 AA 脚本注入:
346
358
  - `count` (integer, 可选): 要跟踪的指令数 (默认: 30)
347
359
  - `initial_state` (object, 可选): 初始寄存器符号,如 `{"rcx": "this_ptr", "rdx": "arg1"}`
348
360
 
349
- #### `ce_call_function(address, args?, return_type?, timeout?)`
350
- 在目标进程中调用函数。**警告: 执行真实代码!**
361
+ #### `ce_call_function(address, args?, call_method?, return_type?, timeout?)`
362
+ 在目标进程中调用函数。**警告: 执行真实代码!**
363
+
364
+ **参数:**
365
+ - `address` (string, 必需): 目标函数地址
366
+ - `args` (array, 可选): 最多 4 个参数(整数或地址表达式)
367
+ - `call_method` (integer, 可选): `executeCodeEx` 调用方式(如 `0`)。省略时 bridge 会自动走兼容回退
368
+ - `return_type` (string, 可选): `byte`, `word`, `dword`, `qword`, `float`, `double`, `pointer`(默认: `qword`)
369
+ - `timeout` (integer, 可选): 超时毫秒数(默认: `5000`)
351
370
 
352
371
  ---
353
372
 
package/ce_mcp_bridge.lua CHANGED
@@ -1500,46 +1500,9 @@ Handlers.execute_lua = function(p)
1500
1500
  local ok, result = pcall(fn)
1501
1501
  if not ok then error("Runtime error: " .. tostring(result)) end
1502
1502
  return { result = result }
1503
- end
1504
-
1505
- Handlers.get_selected_address = function()
1506
- local mv = getMemoryViewForm()
1507
- if mv then
1508
- local dv = mv.DisassemblerView
1509
- if dv and dv.SelectedAddress then
1510
- return { address = Utils.formatHex(dv.SelectedAddress) }
1511
- end
1512
- end
1513
- error("No address selected in Memory View")
1514
- end
1515
-
1516
- -- Handler: get_logs - Retrieve log entries with filtering
1517
- Handlers.get_logs = function(p)
1518
- local count = p.count or 100
1519
- local minLevel = p.min_level or p.minLevel or LogLevel.DEBUG
1520
-
1521
- -- Convert string level to number if needed
1522
- if type(minLevel) == "string" then
1523
- local levelMap = {
1524
- debug = LogLevel.DEBUG,
1525
- info = LogLevel.INFO,
1526
- warn = LogLevel.WARN,
1527
- error = LogLevel.ERROR
1528
- }
1529
- minLevel = levelMap[minLevel:lower()] or LogLevel.DEBUG
1530
- end
1531
-
1532
- local entries = Logger.getEntries(count, minLevel)
1533
- local stats = Logger.getStats()
1534
-
1535
- return {
1536
- entries = entries,
1537
- count = #entries,
1538
- stats = stats
1539
- }
1540
- end
1541
-
1542
- -- Handler: stats - Return comprehensive execution statistics
1503
+ end
1504
+
1505
+ -- Handler: stats - Return comprehensive execution statistics
1543
1506
  Handlers.stats = function(p)
1544
1507
  local metrics = Metrics.getSummary()
1545
1508
  local logStats = Logger.getStats()
@@ -1729,10 +1692,11 @@ Handlers.aob_scan = function(p)
1729
1692
  scanOk = true
1730
1693
  end)
1731
1694
 
1732
- if not scanOk then
1733
- pcall(function() memscan.destroy() end)
1734
- return { count = 0, results = {}, error = "AOB Scan failed" }
1735
- end
1695
+ if not scanOk then
1696
+ pcall(function() foundList.deinitialize() end)
1697
+ pcall(function() memscan.destroy() end)
1698
+ return { count = 0, results = {}, error = "AOB Scan failed" }
1699
+ end
1736
1700
 
1737
1701
  foundList.initialize()
1738
1702
  local totalCount = foundList.Count or 0
@@ -1836,10 +1800,11 @@ Handlers.value_scan = function(p)
1836
1800
  memscan.waitTillDone()
1837
1801
  end)
1838
1802
 
1839
- if not scanOk then
1840
- pcall(function() memscan.destroy() end)
1841
- return { count = 0, results = {}, error = "Scan failed" }
1842
- end
1803
+ if not scanOk then
1804
+ pcall(function() foundList.deinitialize() end)
1805
+ pcall(function() memscan.destroy() end)
1806
+ return { count = 0, results = {}, error = "Scan failed" }
1807
+ end
1843
1808
 
1844
1809
  foundList.initialize()
1845
1810
 
@@ -1860,11 +1825,12 @@ Handlers.value_scan = function(p)
1860
1825
  isInModule = inModule(addrNum)
1861
1826
  end)
1862
1827
 
1863
- table_insert(results, {
1864
- address = Utils.formatHex(addrNum),
1865
- symbol = symbol,
1866
- isStatic = symbol and symbol:find("%.exe%+") ~= nil or symbol:find("%.dll%+") ~= nil
1867
- })
1828
+ table_insert(results, {
1829
+ address = Utils.formatHex(addrNum),
1830
+ symbol = symbol,
1831
+ isStatic = (symbol ~= nil) and
1832
+ (symbol:find("%.exe%+") ~= nil or symbol:find("%.dll%+") ~= nil)
1833
+ })
1868
1834
  end
1869
1835
  end
1870
1836
 
@@ -1986,10 +1952,11 @@ Handlers.scan_new = function(p)
1986
1952
  memscan.waitTillDone()
1987
1953
  end)
1988
1954
 
1989
- if not scanOk then
1990
- pcall(function() memscan.destroy() end)
1991
- error("First scan failed")
1992
- end
1955
+ if not scanOk then
1956
+ pcall(function() foundList.deinitialize() end)
1957
+ pcall(function() memscan.destroy() end)
1958
+ error("First scan failed")
1959
+ end
1993
1960
 
1994
1961
  foundList.initialize()
1995
1962
  local totalCount = foundList.Count or 0
@@ -2142,13 +2109,14 @@ Handlers.scan_results = function(p)
2142
2109
  end
2143
2110
  end)
2144
2111
 
2145
- table_insert(results, {
2146
- index = i,
2147
- address = Utils.formatHex(addrNum),
2148
- symbol = symbol,
2149
- value = currentValue,
2150
- isStatic = symbol and (symbol:find("%.exe%+") ~= nil or symbol:find("%.dll%+") ~= nil)
2151
- })
2112
+ table_insert(results, {
2113
+ index = i,
2114
+ address = Utils.formatHex(addrNum),
2115
+ symbol = symbol,
2116
+ value = currentValue,
2117
+ isStatic = (symbol ~= nil) and
2118
+ (symbol:find("%.exe%+") ~= nil or symbol:find("%.dll%+") ~= nil)
2119
+ })
2152
2120
  end
2153
2121
  end
2154
2122
 
@@ -2333,10 +2301,10 @@ Handlers.enum_modules = function()
2333
2301
  if #r == 0 then
2334
2302
  usedFallback = true
2335
2303
  local mzScan = AOBScan("4D 5A 90 00 03 00 00 00") -- MZ PE header
2336
- if mzScan and mzScan.Count > 0 then
2337
- for i = 0, math_min(mzScan.Count - 1, 50) do
2338
- local addr = tonumber(mzScan[i], 16)
2339
- if addr then
2304
+ if mzScan and mzScan.Count > 0 then
2305
+ for i = 0, math_min(mzScan.Count - 1, 50) do
2306
+ local addr = tonumber(mzScan[i], 16)
2307
+ if addr then
2340
2308
  local peOffset = readInteger(addr + 0x3C)
2341
2309
  local moduleSize = 0
2342
2310
  local realName = nil
@@ -2376,11 +2344,13 @@ Handlers.enum_modules = function()
2376
2344
  path = "",
2377
2345
  source = realName and "export_directory" or "aob_fallback"
2378
2346
  })
2379
- end
2380
- end
2381
- mzScan.destroy()
2382
- end
2383
- end
2347
+ end
2348
+ end
2349
+ end
2350
+ if mzScan then
2351
+ mzScan.destroy()
2352
+ end
2353
+ end
2384
2354
 
2385
2355
  return {
2386
2356
  modules = r,
@@ -6911,12 +6881,25 @@ Handlers.call_function = function(p)
6911
6881
  table_insert(resolvedArgs, 0)
6912
6882
  end
6913
6883
 
6914
- -- Execute the function using CE's executeCodeEx
6915
- -- executeCodeEx(timeout, address, param1, param2, ...)
6916
- local ok, result = pcall(function()
6917
- return executeCodeEx(timeout, funcAddr,
6918
- resolvedArgs[1], resolvedArgs[2], resolvedArgs[3], resolvedArgs[4])
6919
- end)
6884
+ -- Execute function using CE's executeCodeEx.
6885
+ -- If call_method is explicitly provided, use new signature:
6886
+ -- executeCodeEx(callmethod, timeout, address, ...)
6887
+ -- If omitted, try old signature first for backward compatibility:
6888
+ -- executeCodeEx(timeout, address, ...)
6889
+ -- then fallback to new signature.
6890
+ local callMethod = p.call_method
6891
+ local ok, result = false, nil
6892
+ if callMethod ~= nil then
6893
+ ok, result = pcall(executeCodeEx, callMethod, timeout, funcAddr,
6894
+ resolvedArgs[1], resolvedArgs[2], resolvedArgs[3], resolvedArgs[4])
6895
+ else
6896
+ ok, result = pcall(executeCodeEx, timeout, funcAddr,
6897
+ resolvedArgs[1], resolvedArgs[2], resolvedArgs[3], resolvedArgs[4])
6898
+ if not ok then
6899
+ ok, result = pcall(executeCodeEx, 0, timeout, funcAddr,
6900
+ resolvedArgs[1], resolvedArgs[2], resolvedArgs[3], resolvedArgs[4])
6901
+ end
6902
+ end
6920
6903
 
6921
6904
  if not ok then
6922
6905
  return {
@@ -7473,9 +7456,8 @@ local SYNC_COMMANDS = {
7473
7456
  register_symbol = true,
7474
7457
  unregister_symbol = true,
7475
7458
  allocate_memory = true,
7476
- execute_lua = true,
7477
- get_selected_address = true,
7478
- -- Hook commands
7459
+ execute_lua = true,
7460
+ -- Hook commands
7479
7461
  hook_function = true,
7480
7462
  unhook_function = true,
7481
7463
  -- Emulation commands
@@ -7694,12 +7676,27 @@ end
7694
7676
  -- ============ Server Lifecycle ============
7695
7677
  local Server = {}
7696
7678
 
7697
- function Server.stop()
7698
- Utils.debugPrint("Stopping server...")
7699
- Context.serverRunning = false
7700
-
7701
- -- Cleanup all zombie resources (breakpoints, traces, etc.)
7702
- Utils.cleanupZombieState()
7679
+ function Server.stop()
7680
+ Utils.debugPrint("Stopping server...")
7681
+ Context.serverRunning = false
7682
+
7683
+ -- Cleanup UI status timer/label to avoid leaked timers across stop/start cycles
7684
+ if CE_MCP_STATUS_LABEL then
7685
+ pcall(function()
7686
+ if CE_MCP_STATUS_LABEL.timer then
7687
+ CE_MCP_STATUS_LABEL.timer.Enabled = false
7688
+ CE_MCP_STATUS_LABEL.timer.destroy()
7689
+ CE_MCP_STATUS_LABEL.timer = nil
7690
+ end
7691
+ if CE_MCP_STATUS_LABEL.label then
7692
+ CE_MCP_STATUS_LABEL.label.destroy()
7693
+ CE_MCP_STATUS_LABEL.label = nil
7694
+ end
7695
+ end)
7696
+ end
7697
+
7698
+ -- Cleanup all zombie resources (breakpoints, traces, etc.)
7699
+ Utils.cleanupZombieState()
7703
7700
 
7704
7701
  -- Clear address cache
7705
7702
  Utils.clearAddressCache()
package/ce_mcp_server.js CHANGED
@@ -31,7 +31,6 @@ class CEMCPServer {
31
31
  // Check 1: Pipe existence
32
32
  let pipeExists = false;
33
33
  try {
34
- const fs = require('fs');
35
34
  // Try to check if pipe exists (Windows specific)
36
35
  const testClient = new PipeClient();
37
36
  const connected = await testClient.connect(false, 1000);
@@ -270,8 +269,8 @@ class CEMCPServer {
270
269
  // Check if error: has error field, success=false, and no partial results
271
270
  let isError = false;
272
271
  if (typeof result === 'object' && result !== null && result.error) {
273
- const hasPartialResult = ['chain', 'path', 'partialCENotation', 'finalAddress'].some(k => k in result);
274
- isError = result.success === false && !hasPartialResult;
272
+ const hasPartialResult = ['chain', 'path', 'partialCENotation', 'finalAddress', 'final_address', 'ceNotation', 'ce_pointer_notation'].some(k => k in result);
273
+ isError = !hasPartialResult;
275
274
  }
276
275
 
277
276
  const text = isError
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cheatengine",
3
- "version": "5.8.28",
3
+ "version": "5.8.30",
4
4
  "description": "Cheat Engine MCP Server - AI-assisted reverse engineering bridge",
5
5
  "main": "ce_mcp_server.js",
6
6
  "bin": {
@@ -5,7 +5,7 @@
5
5
 
6
6
  const net = require('net');
7
7
  const { EventEmitter } = require('events');
8
- const { Config, HealthMonitor, Logger, log } = require('./base');
8
+ const { Config, HealthMonitor, TimeoutError, Logger, log } = require('./base');
9
9
 
10
10
  // Windows error codes
11
11
  const ERROR_FILE_NOT_FOUND = 2;
@@ -21,6 +21,7 @@ class PipeClient extends EventEmitter {
21
21
  this.socket = null;
22
22
  this.healthMonitor = new HealthMonitor(errorThreshold);
23
23
  this.reconnectTimer = null;
24
+ this.reconnectLoopPromise = null;
24
25
  this.stopReconnectFlag = false;
25
26
  this.connected = false;
26
27
  this.connectionAttempts = 0;
@@ -97,6 +98,8 @@ class PipeClient extends EventEmitter {
97
98
  this.pendingResponse.reject(new Error(`Invalid response size: ${respLen}`));
98
99
  this.pendingResponse = null;
99
100
  this.responseBuffer = Buffer.alloc(0);
101
+ this._close();
102
+ this.healthMonitor.recordError(`Invalid response frame length: ${respLen}`);
100
103
  return;
101
104
  }
102
105
 
@@ -132,21 +135,25 @@ class PipeClient extends EventEmitter {
132
135
  let backoff = 0.5;
133
136
  const maxBackoff = 10.0;
134
137
 
135
- while (!this.stopReconnectFlag) {
136
- if (!this.isValid()) {
137
- this.connectionAttempts++;
138
- this.healthMonitor.recordConnectionAttempt();
139
-
140
- if (await this._tryConnectOnce()) {
141
- backoff = 0.5;
138
+ try {
139
+ while (!this.stopReconnectFlag) {
140
+ if (!this.isValid()) {
141
+ this.connectionAttempts++;
142
+ this.healthMonitor.recordConnectionAttempt();
143
+
144
+ if (await this._tryConnectOnce()) {
145
+ backoff = 0.5;
146
+ } else {
147
+ backoff = Math.min(backoff * 1.5, maxBackoff);
148
+ }
142
149
  } else {
143
- backoff = Math.min(backoff * 1.5, maxBackoff);
150
+ backoff = 0.5;
144
151
  }
145
- } else {
146
- backoff = 0.5;
147
- }
148
152
 
149
- await this._sleep(backoff * 1000);
153
+ await this._sleep(backoff * 1000);
154
+ }
155
+ } finally {
156
+ this.reconnectLoopPromise = null;
150
157
  }
151
158
  }
152
159
 
@@ -155,9 +162,9 @@ class PipeClient extends EventEmitter {
155
162
  }
156
163
 
157
164
  startBackgroundReconnect() {
158
- if (this.reconnectTimer) return;
165
+ if (this.reconnectLoopPromise) return;
159
166
  this.stopReconnectFlag = false;
160
- this._reconnectLoop();
167
+ this.reconnectLoopPromise = this._reconnectLoop();
161
168
  }
162
169
 
163
170
  async connect(force = false, timeout = 3000) {
@@ -245,9 +252,8 @@ class PipeClient extends EventEmitter {
245
252
  const lenBuffer = Buffer.alloc(4);
246
253
  lenBuffer.writeUInt32LE(jsonBytes.length, 0);
247
254
 
248
- // Send request
249
- this.socket.write(lenBuffer);
250
- this.socket.write(jsonBytes);
255
+ // Send request as one framed buffer to avoid split-frame races
256
+ this.socket.write(Buffer.concat([lenBuffer, jsonBytes]));
251
257
 
252
258
  // Wait for response
253
259
  const result = await this._waitForResponse();
@@ -268,7 +274,15 @@ class PipeClient extends EventEmitter {
268
274
 
269
275
  _waitForResponse() {
270
276
  return new Promise((resolve, reject) => {
277
+ const socket = this.socket;
278
+ let closeHandler = null;
271
279
  this.pendingResponse = { resolve, reject };
280
+
281
+ const cleanup = () => {
282
+ if (socket && closeHandler) {
283
+ socket.removeListener('close', closeHandler);
284
+ }
285
+ };
272
286
 
273
287
  // Check if we already have data in buffer (race condition fix)
274
288
  if (this.responseBuffer.length >= 4) {
@@ -281,10 +295,12 @@ class PipeClient extends EventEmitter {
281
295
  const decoded = this._decodeResponse(respData);
282
296
  const result = JSON.parse(decoded);
283
297
  this.pendingResponse = null;
298
+ cleanup();
284
299
  resolve(result);
285
300
  return;
286
301
  } catch (err) {
287
302
  this.pendingResponse = null;
303
+ cleanup();
288
304
  reject(err);
289
305
  return;
290
306
  }
@@ -292,13 +308,14 @@ class PipeClient extends EventEmitter {
292
308
  }
293
309
 
294
310
  // Set up a one-time close handler to reject if socket closes while waiting
295
- const closeHandler = () => {
311
+ closeHandler = () => {
296
312
  this.pendingResponse = null;
313
+ cleanup();
297
314
  reject(new Error('Socket closed while waiting for response'));
298
315
  };
299
316
 
300
- if (this.socket) {
301
- this.socket.once('close', closeHandler);
317
+ if (socket) {
318
+ socket.once('close', closeHandler);
302
319
  }
303
320
  });
304
321
  }
@@ -308,8 +325,9 @@ class PipeClient extends EventEmitter {
308
325
  const startTime = Date.now();
309
326
 
310
327
  // Create timeout promise
328
+ let timeoutHandle = null;
311
329
  const timeoutPromise = new Promise((_, reject) => {
312
- setTimeout(() => {
330
+ timeoutHandle = setTimeout(() => {
313
331
  const elapsedTime = (Date.now() - startTime) / 1000;
314
332
  const errorMsg = `Tool execution timed out after ${elapsedTime.toFixed(2)}s (timeout: ${timeoutSeconds}s)`;
315
333
  log.error(`Timeout: tool=${toolName || 'unknown'}, params=${JSON.stringify(data.params || {})}, elapsed=${elapsedTime.toFixed(2)}s`);
@@ -337,6 +355,10 @@ class PipeClient extends EventEmitter {
337
355
  };
338
356
  }
339
357
  return { error: err.message };
358
+ } finally {
359
+ if (timeoutHandle) {
360
+ clearTimeout(timeoutHandle);
361
+ }
340
362
  }
341
363
  }
342
364
 
@@ -346,6 +368,7 @@ class PipeClient extends EventEmitter {
346
368
 
347
369
  stop() {
348
370
  this.stopReconnectFlag = true;
371
+ this.reconnectLoopPromise = null;
349
372
  if (this.reconnectTimer) {
350
373
  clearTimeout(this.reconnectTimer);
351
374
  this.reconnectTimer = null;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Tool Registry for MCP Tools
3
- * Defines all 60+ tools available in the Cheat Engine MCP Bridge
3
+ * Defines all 55 tools available in the Cheat Engine MCP Bridge
4
4
  */
5
5
 
6
6
  const ToolCategory = {
@@ -108,32 +108,12 @@ class ToolRegistry {
108
108
 
109
109
  this._register(new Tool(
110
110
  'ce_execute_lua',
111
- 'Execute raw Lua code in CE',
111
+ 'Execute Lua code with full access to CE APIs. Returns {result}.',
112
112
  'execute_lua',
113
113
  ToolCategory.SYSTEM,
114
114
  [new ToolParam('code', 'string', 'Lua code to execute', true)]
115
115
  ));
116
116
 
117
- this._register(new Tool(
118
- 'ce_get_selected_address',
119
- 'Get the currently selected address in CE Memory View/Disassembler',
120
- 'get_selected_address',
121
- ToolCategory.SYSTEM
122
- ));
123
-
124
- this._register(new Tool(
125
- 'ce_get_logs',
126
- 'Get log entries from the Cheat Engine MCP Bridge Lua side. ' +
127
- 'Returns: {entries: [{timestamp, level, category, message, data}], count}. ' +
128
- 'Useful for debugging and monitoring bridge operations.',
129
- 'get_logs',
130
- ToolCategory.SYSTEM,
131
- [
132
- new ToolParam('count', 'integer', 'Maximum number of log entries to return (default: 100)', false, 100),
133
- new ToolParam('min_level', 'integer', 'Minimum log level to include: 1=DEBUG, 2=INFO, 3=WARN, 4=ERROR (default: 1)', false, 1),
134
- ]
135
- ));
136
-
137
117
  this._register(new Tool(
138
118
  'ce_list_processes',
139
119
  'List running processes. Use before ce_attach_process to find available targets. ' +
@@ -173,7 +153,7 @@ class ToolRegistry {
173
153
 
174
154
  this._register(new Tool(
175
155
  'ce_read_memory',
176
- 'Read a single memory value. Returns: {address, type, value}. ' +
156
+ 'Read a single memory value. Returns: {value}. ' +
177
157
  'For reading multiple addresses, use ce_read_memory_batch instead for better performance.',
178
158
  'read_memory',
179
159
  ToolCategory.MEMORY,
@@ -189,7 +169,7 @@ class ToolRegistry {
189
169
  '[PERFORMANCE] Read multiple memory addresses in ONE request. ' +
190
170
  'ALWAYS prefer this over multiple ce_read_memory calls for better performance. ' +
191
171
  'Example: [{"address": "game.exe+100", "type": "dword", "id": "hp"}, {"address": "game.exe+104", "type": "float", "id": "mp"}]. ' +
192
- 'Returns: {results: [{id, address, type, value, error?}]}.',
172
+ 'Returns: {results: {id_or_address: {value, error?}}}.',
193
173
  'read_memory_batch',
194
174
  ToolCategory.MEMORY,
195
175
  [new ToolParam('requests', 'array', 'Array of {address, type, id?, size?} objects', true)]
@@ -197,7 +177,7 @@ class ToolRegistry {
197
177
 
198
178
  this._register(new Tool(
199
179
  'ce_write_memory',
200
- 'Write memory value. Returns: {success: true, address, bytes_written}.',
180
+ 'Write memory value. Returns: {success, address}.',
201
181
  'write_memory',
202
182
  ToolCategory.MEMORY,
203
183
  [
@@ -318,7 +298,7 @@ class ToolRegistry {
318
298
 
319
299
  this._register(new Tool(
320
300
  'ce_enum_modules',
321
- 'List all loaded modules (DLLs). Returns: {count, modules: [{name, base, size}]}.',
301
+ 'List all loaded modules (DLLs). Returns: {count, modules: [{name, address, size, path, source}], used_fallback}.',
322
302
  'enum_modules',
323
303
  ToolCategory.SCANNING
324
304
  ));
@@ -395,7 +375,7 @@ class ToolRegistry {
395
375
  'USE WHEN: You already have base+offsets and want to verify the chain works or read the value. ' +
396
376
  'Returns CE notation like \'[[game.exe+123]+10]+20\' for easy copy to CE. ' +
397
377
  'NOT FOR: Discovering pointer paths (use ce_find_pointer_path for discovery). ' +
398
- 'Returns: {base, offsets, final_address, ceNotation, chain: [{level, address, value, symbol}]}.',
378
+ 'Returns: {success, base, offsets, finalAddress, ceNotation, chain: [{level, address, symbol, offset, ptrValue?}], partialCENotation?, failedAtLevel?, error?}.',
399
379
  'resolve_pointer',
400
380
  ToolCategory.SYMBOLS,
401
381
  [
@@ -426,7 +406,7 @@ class ToolRegistry {
426
406
 
427
407
  this._register(new Tool(
428
408
  'ce_get_instruction_info',
429
- 'Get detailed instruction info. Returns: {address, opcode, parameters, bytes, size, isCall, isJump, isRet, isConditionalJump, readsMemory, writesMemory}.',
409
+ 'Get detailed instruction info. Returns: {address, opcode, params, bytes, bytesStr, size, isCall, isJump, isRet, isConditionalJump, parameterValue}.',
430
410
  'get_instruction_info',
431
411
  ToolCategory.DEBUG,
432
412
  [this.ADDR_PARAM]
@@ -757,7 +737,7 @@ class ToolRegistry {
757
737
  'Generate unique AOB signature for code at an address. ' +
758
738
  'USE WHEN: Creating version-independent scripts, documenting code locations for future game updates. ' +
759
739
  'WORKFLOW: Generate signature here -> after game update, use ce_aob_scan to relocate. ' +
760
- 'Returns: {address, signature, mask, unique}.',
740
+ 'Returns: {address, signature, offset_from_start, byte_count, usage_hint}.',
761
741
  'generate_signature',
762
742
  ToolCategory.ANALYSIS,
763
743
  [this.ADDR_PARAM]
@@ -842,6 +822,7 @@ class ToolRegistry {
842
822
  [
843
823
  this.ADDR_PARAM,
844
824
  new ToolParam('args', 'array', 'Function arguments (up to 4 for fastcall). Can be integers or address expressions.', false, []),
825
+ new ToolParam('call_method', 'integer', 'Optional calling method for executeCodeEx (e.g. 0=default/stdcall-like). If omitted, bridge auto-fallbacks for compatibility.'),
845
826
  new ToolParam('timeout', 'integer', 'Timeout in milliseconds', false, 5000),
846
827
  new ToolParam('return_type', 'string', 'How to interpret return value', false, 'qword',
847
828
  ['byte', 'word', 'dword', 'qword', 'float', 'double', 'pointer']),