cheatengine 5.8.20 → 5.8.22

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/ce_mcp_bridge.lua CHANGED
@@ -26,15 +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
- VERSION = "5.8.4",
32
- -- Pipe name: supports environment variable override for anti-detection
33
- -- Set CE_MCP_PIPE_NAME environment variable to customize
34
- PIPE_NAME = os.getenv("CE_MCP_PIPE_NAME") or "ce_mcp_bridge",
35
- -- Authentication token: optional security layer for pipe communication
36
- -- Set CE_MCP_AUTH_TOKEN environment variable to enable authentication
37
- 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,
38
37
  DEBUG_MODE = false,
39
38
  MAX_MESSAGE_SIZE = 10 * 1024 * 1024,
40
39
  PIPE_BUFFER_SIZE = 1024 * 1024,
@@ -1459,17 +1458,16 @@ local function getDebuggerStatus()
1459
1458
  return status
1460
1459
  end
1461
1460
 
1462
- Handlers.ping = function()
1463
- return {
1464
- status = "ok",
1465
- version = Config.VERSION,
1466
- timestamp = os.time(),
1467
- process = process or "N/A",
1468
- pid = getOpenedProcessID() or 0,
1469
- connections = Context.connectionCount,
1470
- debugger = getDebuggerStatus()
1471
- }
1472
- 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
1473
1471
 
1474
1472
  Handlers.get_process_info = function()
1475
1473
  -- Refresh symbol table using ModuleManager
@@ -1558,17 +1556,16 @@ Handlers.stats = function(p)
1558
1556
  })
1559
1557
  end
1560
1558
 
1561
- return {
1562
- version = Config.VERSION,
1563
- uptime = metrics.uptime,
1564
- commands = metrics.commands,
1565
- summary = metrics.summary,
1566
- cache = metrics.cache,
1567
- logging = logStats,
1568
- scanSessions = {
1569
- active = scanSessionCount,
1570
- maxAllowed = Config.MAX_SCAN_SESSIONS,
1571
- 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
1572
1569
  },
1573
1570
  connections = Context.connectionCount,
1574
1571
  debugger = getDebuggerStatus()
@@ -7488,121 +7485,174 @@ function Pipe.executeRequest(req)
7488
7485
  end
7489
7486
  end
7490
7487
 
7491
- function Pipe.workerLoop()
7492
- Utils.debugPrint("Worker started")
7493
- local consecutiveErrors = 0
7494
-
7495
- while Context.serverRunning do
7496
- local acceptOk, acceptErr = pcall(function()
7497
- Context.pipeServer.acceptConnection()
7498
- end)
7499
-
7500
- if not acceptOk then
7501
- consecutiveErrors = consecutiveErrors + 1
7502
- if consecutiveErrors > Config.MAX_CONSECUTIVE_ERRORS then
7503
- Utils.debugPrint("Too many consecutive errors, restarting pipe...")
7504
- pcall(function() Context.pipeServer.destroy() end)
7505
- Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7506
- consecutiveErrors = 0
7507
- end
7508
- if Context.serverRunning then
7509
- sleep(Config.HEARTBEAT_INTERVAL)
7510
- end
7511
- else
7512
- consecutiveErrors = 0
7513
- Context.connectionCount = Context.connectionCount + 1
7514
- Context.lastActivityTime = os_clock() -- Heartbeat: record connection time
7515
- Utils.debugPrint("Client connected (#" .. Context.connectionCount .. ")")
7516
-
7517
- local sessionErrors = 0
7518
-
7519
- while Context.serverRunning and Context.pipeServer and Context.pipeServer.Valid do
7520
- Utils.periodicCleanup()
7521
-
7522
- -- Read size
7523
- local ok, size = false, nil
7524
- pcall(function()
7525
- Context.pipeServer.lock()
7526
- ok, size = pcall(Context.pipeServer.readDword)
7527
- Context.pipeServer.unlock()
7528
- end)
7529
-
7530
- if not ok or not size or size == 0 then
7531
- sessionErrors = sessionErrors + 1
7532
- if sessionErrors >= Config.MAX_SESSION_ERRORS then
7533
- Utils.debugPrint("Too many session errors, disconnecting client")
7534
- break
7535
- end
7536
- sleep(10)
7537
- goto continue
7538
- end
7539
-
7540
- if size >= Config.MAX_MESSAGE_SIZE then
7541
- Utils.debugPrint("Message too large: " .. size)
7542
- break
7543
- end
7544
-
7545
- sessionErrors = 0
7546
-
7547
- -- Read payload
7548
- local ok2, payload = false, nil
7549
- pcall(function()
7550
- Context.pipeServer.lock()
7551
- ok2, payload = pcall(Context.pipeServer.readString, size)
7552
- Context.pipeServer.unlock()
7553
- end)
7554
-
7555
- if ok2 and payload and #payload == size then
7556
- local req = JSON.decode(payload)
7557
- local resp = nil
7558
-
7559
- -- Use optimized request execution (sync on demand)
7560
- resp = Pipe.executeRequest(req)
7561
-
7562
- local respStr = JSON.encode(resp)
7563
- local respLen = #respStr
7564
-
7565
- if respLen > Config.MAX_MESSAGE_SIZE then
7566
- respStr = JSON.encode({ error = "Response too large" })
7567
- respLen = #respStr
7568
- end
7569
-
7570
- local writeOk = false
7571
- pcall(function()
7572
- Context.pipeServer.lock()
7573
- writeOk = pcall(function()
7574
- Context.pipeServer.writeDword(respLen)
7575
- Context.pipeServer.writeString(respStr, false)
7576
- end)
7577
- Context.pipeServer.unlock()
7578
- end)
7579
-
7580
- if writeOk then
7581
- Context.lastActivityTime = os_clock() -- Heartbeat: update on successful communication
7582
- else
7583
- Utils.debugPrint("Failed to write response")
7584
- break
7585
- end
7586
- else
7587
- Utils.debugPrint("Failed to read payload")
7588
- break
7589
- end
7590
-
7591
- ::continue::
7592
- end
7593
-
7594
- pcall(function()
7595
- if Context.pipeServer and Context.pipeServer.Valid then
7596
- Context.pipeServer.disconnect()
7597
- end
7598
- end)
7599
- Context.lastActivityTime = 0 -- Heartbeat: reset on disconnect
7600
- Utils.debugPrint("Client disconnected, waiting for new connection...")
7601
- sleep(50)
7602
- end
7603
- end
7604
- Utils.debugPrint("Worker stopped")
7605
- end
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
+ -- Handshake: send ready signal
7531
+ local handshakeOk = pcall(function()
7532
+ Context.pipeServer.lock()
7533
+ local ok = pcall(function()
7534
+ local readyMsg = JSON.encode({ ready = true, timestamp = os.time() })
7535
+ Context.pipeServer.writeDword(#readyMsg)
7536
+ Context.pipeServer.writeString(readyMsg, false)
7537
+ end)
7538
+ Context.pipeServer.unlock()
7539
+ return ok
7540
+ end)
7541
+
7542
+ if not handshakeOk then
7543
+ Utils.debugPrint("Handshake failed, disconnecting client")
7544
+ pcall(function()
7545
+ if Context.pipeServer and Context.pipeServer.Valid then
7546
+ Context.pipeServer.disconnect()
7547
+ end
7548
+ end)
7549
+ sleep(50)
7550
+ goto next_connection
7551
+ end
7552
+
7553
+ Utils.debugPrint("Handshake completed")
7554
+ local sessionErrors = 0
7555
+
7556
+ while Context.serverRunning and Context.pipeServer and Context.pipeServer.Valid do
7557
+ Utils.periodicCleanup()
7558
+
7559
+ -- Read size
7560
+ local ok, size = false, nil
7561
+ pcall(function()
7562
+ Context.pipeServer.lock()
7563
+ ok, size = pcall(Context.pipeServer.readDword)
7564
+ Context.pipeServer.unlock()
7565
+ end)
7566
+
7567
+ if not ok or not size or size == 0 then
7568
+ sessionErrors = sessionErrors + 1
7569
+ if sessionErrors >= Config.MAX_SESSION_ERRORS then
7570
+ Utils.debugPrint("Too many session errors, disconnecting client")
7571
+ -- Send error before disconnecting
7572
+ sendErrorResponse("Too many consecutive read errors")
7573
+ break
7574
+ end
7575
+ sleep(10)
7576
+ goto continue
7577
+ end
7578
+
7579
+ if size >= Config.MAX_MESSAGE_SIZE then
7580
+ Utils.debugPrint("Message too large: " .. size)
7581
+ sendErrorResponse("Message too large: " .. size .. " bytes (max: " .. Config.MAX_MESSAGE_SIZE .. ")")
7582
+ break
7583
+ end
7584
+
7585
+ sessionErrors = 0
7586
+
7587
+ -- Read payload
7588
+ local ok2, payload = false, nil
7589
+ pcall(function()
7590
+ Context.pipeServer.lock()
7591
+ ok2, payload = pcall(Context.pipeServer.readString, size)
7592
+ Context.pipeServer.unlock()
7593
+ end)
7594
+
7595
+ if ok2 and payload and #payload == size then
7596
+ local decodeOk, req = pcall(JSON.decode, payload)
7597
+ if not decodeOk or not req then
7598
+ Utils.debugPrint("Failed to decode JSON payload")
7599
+ sendErrorResponse("Invalid JSON payload")
7600
+ goto continue
7601
+ end
7602
+
7603
+ local resp = nil
7604
+
7605
+ -- Use optimized request execution (sync on demand)
7606
+ resp = Pipe.executeRequest(req)
7607
+
7608
+ local respStr = JSON.encode(resp)
7609
+ local respLen = #respStr
7610
+
7611
+ if respLen > Config.MAX_MESSAGE_SIZE then
7612
+ respStr = JSON.encode({ error = "Response too large" })
7613
+ respLen = #respStr
7614
+ end
7615
+
7616
+ local writeOk = false
7617
+ pcall(function()
7618
+ Context.pipeServer.lock()
7619
+ writeOk = pcall(function()
7620
+ Context.pipeServer.writeDword(respLen)
7621
+ Context.pipeServer.writeString(respStr, false)
7622
+ end)
7623
+ Context.pipeServer.unlock()
7624
+ end)
7625
+
7626
+ if writeOk then
7627
+ Context.lastActivityTime = os_clock() -- Heartbeat: update on successful communication
7628
+ else
7629
+ Utils.debugPrint("Failed to write response")
7630
+ break
7631
+ end
7632
+ else
7633
+ Utils.debugPrint("Failed to read payload: expected " .. size .. " bytes")
7634
+ sendErrorResponse("Failed to read payload: expected " .. size .. " bytes, got " .. tostring(#(payload or "")))
7635
+ -- Don't break, allow client to retry
7636
+ sleep(50)
7637
+ end
7638
+
7639
+ ::continue::
7640
+ end
7641
+
7642
+ pcall(function()
7643
+ if Context.pipeServer and Context.pipeServer.Valid then
7644
+ Context.pipeServer.disconnect()
7645
+ end
7646
+ end)
7647
+ Context.lastActivityTime = 0 -- Heartbeat: reset on disconnect
7648
+ Utils.debugPrint("Client disconnected, waiting for new connection...")
7649
+ sleep(50)
7650
+ end
7651
+
7652
+ ::next_connection::
7653
+ end
7654
+ Utils.debugPrint("Worker stopped")
7655
+ end
7606
7656
 
7607
7657
  -- ============ Server Lifecycle ============
7608
7658
  local Server = {}
@@ -7631,13 +7681,13 @@ function Server.stop()
7631
7681
  Utils.debugPrint("Server stopped (handled " .. Context.connectionCount .. " connections)")
7632
7682
  end
7633
7683
 
7634
- function Server.start()
7635
- Server.stop() -- This calls cleanupZombieState()
7636
-
7637
- -- Additional cleanup for fresh start
7638
- Utils.debugPrint("Starting MCP Bridge v" .. Config.VERSION)
7639
-
7640
- Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7684
+ function Server.start()
7685
+ Server.stop() -- This calls cleanupZombieState()
7686
+
7687
+ -- Additional cleanup for fresh start
7688
+ Utils.debugPrint("Starting MCP Bridge")
7689
+
7690
+ Context.pipeServer = createPipe(Config.PIPE_NAME, Config.PIPE_BUFFER_SIZE, Config.PIPE_BUFFER_SIZE)
7641
7691
  if not Context.pipeServer or not Context.pipeServer.Valid then
7642
7692
  print("[CE-MCP] Failed to create pipe: " .. Config.PIPE_NAME)
7643
7693
  return false
@@ -7670,15 +7720,14 @@ function Server.stats()
7670
7720
  bpCount = bpCount + 1
7671
7721
  end
7672
7722
 
7673
- return {
7674
- running = Context.serverRunning,
7675
- connections = Context.connectionCount,
7676
- cached_symbols = symbolCacheCount,
7677
- cached_modules = moduleCacheCount,
7678
- active_breakpoints = bpCount,
7679
- version = Config.VERSION
7680
- }
7681
- end
7723
+ return {
7724
+ running = Context.serverRunning,
7725
+ connections = Context.connectionCount,
7726
+ cached_symbols = symbolCacheCount,
7727
+ cached_modules = moduleCacheCount,
7728
+ active_breakpoints = bpCount
7729
+ }
7730
+ end
7682
7731
 
7683
7732
  -- ============ Global API ============
7684
7733
  -- Cleanup old instance
@@ -7697,19 +7746,18 @@ if CE_MCP_STATUS_LABEL then
7697
7746
  end)
7698
7747
  end
7699
7748
 
7700
- CE_MCP = {
7701
- start = Server.start,
7702
- stop = Server.stop,
7703
- stats = Server.stats,
7704
- version = Config.VERSION,
7705
- -- Logger API
7706
- Logger = Logger,
7707
- LogLevel = LogLevel,
7708
- -- Expose for debugging
7709
- _config = Config,
7710
- _context = Context,
7711
- _handlers = Handlers,
7712
- }
7749
+ CE_MCP = {
7750
+ start = Server.start,
7751
+ stop = Server.stop,
7752
+ stats = Server.stats,
7753
+ -- Logger API
7754
+ Logger = Logger,
7755
+ LogLevel = LogLevel,
7756
+ -- Expose for debugging
7757
+ _config = Config,
7758
+ _context = Context,
7759
+ _handlers = Handlers,
7760
+ }
7713
7761
 
7714
7762
  CE_MCP_BRIDGE_INSTANCE = CE_MCP
7715
7763
 
@@ -7751,16 +7799,15 @@ function StatusLabel.create()
7751
7799
  label.Top = 5
7752
7800
  end
7753
7801
 
7754
- -- Click to show stats
7755
- label.OnClick = function()
7756
- local stats = Server.stats()
7757
- local connected = Context.lastActivityTime > 0 and
7758
- (os_clock() - Context.lastActivityTime) < Config.HEARTBEAT_TIMEOUT
7759
- showMessage(string_format("CE MCP v%s\nStatus: %s\nConnections: %d",
7760
- Config.VERSION,
7761
- connected and "Connected" or "Waiting",
7762
- stats.connections or 0))
7763
- end
7802
+ -- Click to show stats
7803
+ label.OnClick = function()
7804
+ local stats = Server.stats()
7805
+ local connected = Context.lastActivityTime > 0 and
7806
+ (os_clock() - Context.lastActivityTime) < Config.HEARTBEAT_TIMEOUT
7807
+ showMessage(string_format("CE MCP\nStatus: %s\nConnections: %d",
7808
+ connected and "Connected" or "Waiting",
7809
+ stats.connections or 0))
7810
+ end
7764
7811
 
7765
7812
  StatusLabel.label = label
7766
7813
  StatusLabel.updateColor(false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cheatengine",
3
- "version": "5.8.20",
3
+ "version": "5.8.22",
4
4
  "description": "Cheat Engine MCP Server - AI-assisted reverse engineering bridge",
5
5
  "main": "ce_mcp_server.js",
6
6
  "bin": {
@@ -86,30 +86,32 @@ class PipeClient extends EventEmitter {
86
86
  _handleData(data) {
87
87
  this.responseBuffer = Buffer.concat([this.responseBuffer, data]);
88
88
 
89
- // Try to parse response
90
- if (this.pendingResponse && this.responseBuffer.length >= 4) {
91
- const respLen = this.responseBuffer.readUInt32LE(0);
92
-
93
- if (respLen === 0 || respLen > Config.MAX_RESPONSE_SIZE) {
94
- this.pendingResponse.reject(new Error(`Invalid response size: ${respLen}`));
95
- this.pendingResponse = null;
96
- this.responseBuffer = Buffer.alloc(0);
97
- return;
98
- }
89
+ // Try to parse response only if we have a pending request
90
+ if (!this.pendingResponse || this.responseBuffer.length < 4) {
91
+ return;
92
+ }
93
+
94
+ const respLen = this.responseBuffer.readUInt32LE(0);
95
+
96
+ if (respLen === 0 || respLen > Config.MAX_RESPONSE_SIZE) {
97
+ this.pendingResponse.reject(new Error(`Invalid response size: ${respLen}`));
98
+ this.pendingResponse = null;
99
+ this.responseBuffer = Buffer.alloc(0);
100
+ return;
101
+ }
99
102
 
100
- if (this.responseBuffer.length >= 4 + respLen) {
101
- const respData = this.responseBuffer.slice(4, 4 + respLen);
102
- this.responseBuffer = this.responseBuffer.slice(4 + respLen);
103
-
104
- try {
105
- const decoded = this._decodeResponse(respData);
106
- const result = JSON.parse(decoded);
107
- this.pendingResponse.resolve(result);
108
- } catch (err) {
109
- this.pendingResponse.reject(err);
110
- }
111
- this.pendingResponse = null;
103
+ if (this.responseBuffer.length >= 4 + respLen) {
104
+ const respData = this.responseBuffer.slice(4, 4 + respLen);
105
+ this.responseBuffer = this.responseBuffer.slice(4 + respLen);
106
+
107
+ try {
108
+ const decoded = this._decodeResponse(respData);
109
+ const result = JSON.parse(decoded);
110
+ this.pendingResponse.resolve(result);
111
+ } catch (err) {
112
+ this.pendingResponse.reject(err);
112
113
  }
114
+ this.pendingResponse = null;
113
115
  }
114
116
  }
115
117
 
@@ -258,7 +260,27 @@ class PipeClient extends EventEmitter {
258
260
  _waitForResponse() {
259
261
  return new Promise((resolve, reject) => {
260
262
  this.pendingResponse = { resolve, reject };
261
- this.responseBuffer = Buffer.alloc(0);
263
+
264
+ // Check if we already have data in buffer (race condition fix)
265
+ if (this.responseBuffer.length >= 4) {
266
+ const respLen = this.responseBuffer.readUInt32LE(0);
267
+ if (respLen > 0 && respLen <= Config.MAX_RESPONSE_SIZE && this.responseBuffer.length >= 4 + respLen) {
268
+ const respData = this.responseBuffer.slice(4, 4 + respLen);
269
+ this.responseBuffer = this.responseBuffer.slice(4 + respLen);
270
+
271
+ try {
272
+ const decoded = this._decodeResponse(respData);
273
+ const result = JSON.parse(decoded);
274
+ this.pendingResponse = null;
275
+ resolve(result);
276
+ return;
277
+ } catch (err) {
278
+ this.pendingResponse = null;
279
+ reject(err);
280
+ return;
281
+ }
282
+ }
283
+ }
262
284
  });
263
285
  }
264
286