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 +1 -1
- package/README_CN.md +42 -42
- package/ce_mcp_bridge.lua +343 -269
- package/package.json +1 -1
- package/src/tool-registry.js +4 -3
package/README.md
CHANGED
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": ["
|
|
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
|
-
|
|
1711
|
-
local
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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
|
|
1720
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
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
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
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
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
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
|
-
|
|
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,
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
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
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
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
package/src/tool-registry.js
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
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,
|