cheatengine 5.8.25 → 5.8.27

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
@@ -31,7 +31,7 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.js <--Named Pipe--> ce_mcp_bridge.lua (CE)
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
  }
package/README_CN.md CHANGED
@@ -31,7 +31,7 @@ AI <--MCP/JSON-RPC--> ce_mcp_server.py <--命名管道--> ce_mcp_bridge.lua (CE)
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
  }
@@ -383,47 +383,47 @@ Hook 函数以拦截调用并捕获参数。
383
383
 
384
384
  ## 推荐工作流
385
385
 
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
- ```
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
+ ```
427
427
 
428
428
  ---
429
429
 
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,148 +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, waiting for new connection...")
7625
- sleep(50)
7626
- end
7627
- end
7628
- Utils.debugPrint("Worker stopped")
7629
- 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
7630
7693
 
7631
7694
  -- ============ Server Lifecycle ============
7632
7695
  local Server = {}
@@ -7655,13 +7718,13 @@ function Server.stop()
7655
7718
  Utils.debugPrint("Server stopped (handled " .. Context.connectionCount .. " connections)")
7656
7719
  end
7657
7720
 
7658
- function Server.start()
7659
- Server.stop() -- This calls cleanupZombieState()
7660
-
7661
- -- Additional cleanup for fresh start
7662
- Utils.debugPrint("Starting MCP Bridge")
7663
-
7664
- 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)
7665
7728
  if not Context.pipeServer or not Context.pipeServer.Valid then
7666
7729
  print("[CE-MCP] Failed to create pipe: " .. Config.PIPE_NAME)
7667
7730
  return false
@@ -7694,14 +7757,14 @@ function Server.stats()
7694
7757
  bpCount = bpCount + 1
7695
7758
  end
7696
7759
 
7697
- return {
7698
- running = Context.serverRunning,
7699
- connections = Context.connectionCount,
7700
- cached_symbols = symbolCacheCount,
7701
- cached_modules = moduleCacheCount,
7702
- active_breakpoints = bpCount
7703
- }
7704
- 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
7705
7768
 
7706
7769
  -- ============ Global API ============
7707
7770
  -- Cleanup old instance
@@ -7720,18 +7783,18 @@ if CE_MCP_STATUS_LABEL then
7720
7783
  end)
7721
7784
  end
7722
7785
 
7723
- CE_MCP = {
7724
- start = Server.start,
7725
- stop = Server.stop,
7726
- stats = Server.stats,
7727
- -- Logger API
7728
- Logger = Logger,
7729
- LogLevel = LogLevel,
7730
- -- Expose for debugging
7731
- _config = Config,
7732
- _context = Context,
7733
- _handlers = Handlers,
7734
- }
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
+ }
7735
7798
 
7736
7799
  CE_MCP_BRIDGE_INSTANCE = CE_MCP
7737
7800
 
@@ -7756,15 +7819,26 @@ function StatusLabel.create()
7756
7819
  label.Cursor = crHandPoint
7757
7820
 
7758
7821
  if commentBtn then
7759
- label.Parent = commentBtn.Parent
7760
- -- Anchor to CommentButton's left side
7761
- label.AnchorSideRight.Control = commentBtn
7762
- label.AnchorSideRight.Side = asrLeft
7763
- label.AnchorSideTop.Control = commentBtn
7764
- label.AnchorSideTop.Side = asrTop
7765
- label.Anchors = "[akTop,akRight]"
7766
- label.BorderSpacing.Right = 5
7767
- 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
7768
7842
  else
7769
7843
  -- Fallback: top right
7770
7844
  label.Parent = mainForm
@@ -7773,15 +7847,15 @@ function StatusLabel.create()
7773
7847
  label.Top = 5
7774
7848
  end
7775
7849
 
7776
- -- Click to show stats
7777
- label.OnClick = function()
7778
- local stats = Server.stats()
7779
- local connected = Context.lastActivityTime > 0 and
7780
- (os_clock() - Context.lastActivityTime) < Config.HEARTBEAT_TIMEOUT
7781
- showMessage(string_format("CE MCP\nStatus: %s\nConnections: %d",
7782
- connected and "Connected" or "Waiting",
7783
- stats.connections or 0))
7784
- 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
7785
7859
 
7786
7860
  StatusLabel.label = label
7787
7861
  StatusLabel.updateColor(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cheatengine",
3
- "version": "5.8.25",
3
+ "version": "5.8.27",
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,