Bridge Cheat Engine 7.5's Lua scripting to TCP so an agent (OPENCODE, WSL, or local Python) can inspect and manipulate game memory and rebind a loaded cheat table without leaving CE.
| This repo is | This repo is not |
|---|---|
CE remote: ce_server.lua + windows_relay.py + client.py |
A shipped full debugger / breakpoint push protocol |
| Native commands for memory, AOB, GroupScan, address list, dissects | Offline bulk .CT generation or agent saveTable |
Game-agnostic skills under skills/ (how to operate CE remote) |
Per-game research dumps under skills/ |
Dying Light 2 knowledge under docs/game/DyingLight2/ (only game tree so far) |
Default “use UE GEngine/GAS skills for every title” |
Server version: ce_server.lua reports via getVersion (currently v1.8.4, includes GroupScan + restartable pipe). Re-Execute the script in CE to restart the pipe server — do not rely on closing the Lua console (that does not stop the background thread).
Docs hub: docs/README.md — migration, hazards, group scan, game knowledge.
Historical design only: SOLUTION.md (early designs including remote BP; not the shipped product).
Hazards / non-goals (canonical): docs/NONGOALS-AND-HAZARDS.md.
Defaults: client/relay use localhost:8888 and a 30s socket timeout. Lab examples may use another host/port (e.g. 192.168.176.1:8000); always pass --host / --port when needed. Use --timeout 120 (or higher) for alSetActive, large AOBScan / GroupScan, and large stClone.
WSL (Agent) Windows Cheat Engine
┌─────────┐ TCP ┌──────────────┐ Named ┌──────────────┐
│ client.py├───────>│windows_relay│───Pipe──>│ ce_server.lua │
│ (agent) │ :8888 │ .py │ │ (in CE) │
└─────────┘ └──────────────┘ └──────────────┘
│
┌─────────┴─────────┐
│ createThread │
│ Background thread │
│ (CE UI responsive)│
└───────────────────┘
The server runs in a background thread via CE's createThread API, keeping
CE's UI responsive. Each client connection gets its own pipe instance (CE
creates single-instance pipes; after a client disconnects, the pipe is
destroyed and a new one is created).
In Cheat Engine, go to Execute Lua Script (Ctrl+L) and paste the contents
of ce_server.lua, then run it. You should see no errors (check CE's Lua
console).
CE stays responsive — the script uses createThread to run the pipe server
on a background thread. CE's UI remains usable while the server is running.
Open a terminal on Windows (cmd, PowerShell, or from WSL via
python.exe) and run:
python windows_relay.py
Default: listens on 127.0.0.1:8888, connects to \\.\pipe\UEScanRemote.
If connecting from a separate machine (or native Linux), bind to all interfaces:
python windows_relay.py --bind 0.0.0.0
Important: The relay opens the named pipe using synchronous I/O. Do
not call SetNamedPipeHandleState with PIPE_NOWAIT — the pipe must
stay in blocking mode for correct operation.
python client.py --cmd "ping"
python client.py --cmd "getVersion" # expect ce-server v1.8.3 …
python client.py --cmd "tableStatus"
python client.py --cmd "readQword 7FF6A1B2C000"
python client.py -i # interactive shell
Session log (optional): CE_SESSION_LOG=1 python client.py --cmd "ping" appends REQ/RSP/ERR to logs/ce-session.log, or pass --session-log /path. Useful when blaming a hung/dead server.
Python helpers: from client import CERemote — methods mirror the commands below (al_dump, group_scan, aob_scan, …). See client.py for signatures and per-call timeouts.
Authoritative list is also returned by help (pipe-separated). Keep this table in sync when adding server commands.
| Command | Description |
|---|---|
ping |
Returns "pong" |
getVersion |
Server version string (e.g. ce-server v1.8.3 (CE 7.5 groupscan)) |
help |
List available commands |
tableStatus |
Process, pid, address-list count, structure count (main thread) |
debugSync |
Smoke-test main-thread synchronize path |
alDump [offset] [limit] |
Address-list inventory TSV (no script bodies); CLASS=AA/EXPR/… |
alGet <id> |
One memrec detail (metadata + value sample) |
alResolve <id> |
Live CurrentAddress / readable / value |
alSetDesc <id> <text> |
Rename/set description |
alSetAddress <id> <expr> |
Set address expression (keeps offsets) |
alSetOffsets <id> <hex,hex> |
Set pointer offset chain |
alSetType <id> <n> |
Set variable type integer |
alGetScript <id> [off] [len] |
AA script slice as hex |
alSetScriptBegin/Chunk/Commit/Abort |
Chunked AA script replace |
aaCheck <id> |
Syntax-check AA script |
alSetActive <id> 0|1 [nocheck] |
Enable/disable (AA enable runs aaCheck unless nocheck) |
alDisableSoft <id> |
Disable without running [Disable] section |
alApply stop=1 hex=… |
Batch setDesc/setAddress/setOffsets/setType (one sync) |
alAudit [n] |
Recent command audit ring buffer |
symGet / symSet |
Resolve / registerSymbol helpers |
stDump |
List global dissect structures (name/size/elems) |
stFind <name> |
Find structure by exact name |
stGet <name> [elemOff] [elemLimit] |
Dump structure elements (default limit 500) |
stEnsureSeed |
Ensure DO_NOT_DELETE_PLACEHOLDER exists (empty-list crash guard) |
stClone <src> <dst> |
Clone structure definition (src -> dst / src|dst OK) |
stBegin / stEnd <name> |
Batch beginUpdate / endUpdate (commit before rename) |
stUpsertElem … |
Insert/update element at offset (prefer name|off|… form) |
stClearElements <name> |
Destroy all elements on a structure (not the placeholder) |
stSetName <old> <new> |
Rename after stEnd (refuses mid-edit) |
readByte <hexaddr> |
Read 1 byte as hex |
readBytes <hexaddr> <size> |
Returns hex-encoded bytes |
readQword <hexaddr> |
Read 8 bytes as hex |
readDword <hexaddr> |
Read 4 bytes as hex |
readString <hexaddr> <maxlen> |
Read null-terminated string |
writeBytes <hexaddr> <hexbytes> |
Write hex bytes (e.g. 90 90) |
getAddress <name> |
Resolve module+offset or symbol to hex address |
resolveSymbol <name> |
Alias for getAddress |
AOBScan <hexpattern> |
Array-of-byte scan (** wildcards allowed); returns addresses |
GroupScan <command> |
CE grouped/structure scan (main thread, ≥ v1.8.3); see docs/CE-GROUP-SCAN.md |
enumModules |
List loaded modules |
runScript <lua_code> |
Execute arbitrary CE Lua code |
runScriptSafe <code> |
runScript with banned-API string filter |
close |
Disconnect |
Prefer native commands over runScript for the same job. Any other CE Lua API can still be attempted via runScript (with risk — see hazards).
Port a loaded .CT to a new game build over this remote: rebind AOBs/scripts, expression/pointer rows, and dissect definitions. The user saves in CE when satisfied (no agent saveTable pipeline).
| Resource | Role |
|---|---|
docs/TABLE-MIGRATE.md |
Full command reference, wire formats, algorithm, crash rules |
skills/ce-table-migrate/SKILL.md |
Agent playbook (AA → enable → pointers → structs) |
skills/ce-table-remote/SKILL.md |
Foundation: sync_call, seed, rename-after-commit |
client.py |
CERemote helpers (al_*, st_*, script chunking, per-call timeout) |
from client import CERemote
# Defaults: localhost:8888. Lab may use another host/port.
ce = CERemote("localhost", 8888, timeout=120)
print(ce.table_status())
print(ce.al_dump(limit=20)[:500])
ce.st_ensure_seed() # if stCount was 0 — empty dissect list crashes CE
# print(ce.group_scan("F:0.34 F:0.34 W:16 F:0.1 F:0.1")) # needs ≥ v1.8.3; timeout 180Order: fix/enable bootstrap AA (symbols) → fix EXPR/POINTER rows → validate structures.
Timeouts: use ≥ 120s for alSetActive, large AOBScan / GroupScan, large stClone.
Structures: stEnd before stSetName; never getStructure("name"); keep DO_NOT_DELETE_PLACEHOLDER.
| Requirement | Notes |
|---|---|
| Cheat Engine 7.5 | Must be able to execute Lua scripts (Ctrl+L). |
Python 3.6+ (python) |
Must be on PATH. Check with python --version in cmd. |
| Named pipe access | Standard users have it by default. No admin needed for the pipe itself. |
| Port 8888 TCP | Not opened by default (relay binds to 127.0.0.1). Only needed if using --bind 0.0.0.0. |
| Requirement | Notes |
|---|---|
Python 3.6+ (python3) |
Pre-installed on Ubuntu/Debian WSL. Check with python3 --version. |
| TCP to Windows host | WSL2 forwards localhost to Windows automatically. WSL1 shares the Windows network stack. |
- WSL2:
localhost(e.g.,localhost:8888) is auto-forwarded to the Windows host. No special configuration needed. - WSL1: Shares the Windows IP stack —
localhostworks directly. - Native Linux (dual boot): Use
--bind 0.0.0.0on the relay and--host <windows-ip>on the client. May need to allow TCP port in Windows Defender Firewall. - Firewall: If you change
--bindto0.0.0.0, you may need to allow inbound TCP/8888 in Windows Defender Firewall.
- Open CE →
Ctrl+L→ pastece_server.lua→ Run - Windows terminal:
python windows_relay.py - WSL:
python client.py --cmd "readQword 7FF6A1B2C000"
powershell.exe -Command "Start-Process python -ArgumentList 'windows_relay.py' -WindowStyle Hidden"Create a task that runs python C:\path\to\windows_relay.py at startup.
Every CE Lua function used in ce_server.lua was verified against the CE 7.5
source code at /mnt/y/Lazarus/Projects/cheat-engine-7.5/Cheat Engine/.
| Function | Source | Behavior |
|---|---|---|
createPipe(name, inSize, outSize) |
luapipeserver.pas:121 |
Returns a pipe object, or nil on failure. Creates a single-instance pipe (nMaxInstances=1). |
pipe.valid |
luapipeserver.pas:25 |
true if the handle is valid. |
pipe.acceptConnection() |
luapipeserver.pas:115 |
Blocks until a client connects (calls ConnectNamedPipe). Does not call DisconnectNamedPipe between reconnections. |
pipe.connected |
luapipe.pas:64 |
false when client disconnects. |
pipe.readBytes(size) |
luapipe.pas:451 |
Returns byte table (1-indexed), or nil on failure. Uses synchronous ReadFile by default (foverlapped=false). |
pipe.writeBytes(table, size) |
luapipe.pas:413 |
Writes bytes from table. Returns count or 0. |
pipe.writeString(str, incZero) |
luapipe.pas:738 |
Writes string; incZero appends null terminator. |
pipe.destroy() |
luapipe.pas:80 (destructor) |
Closes the pipe handle and frees resources. Explicitly needed before recreating (since acceptConnection can't reset without DisconnectNamedPipe). |
| Function | Source | Behavior |
|---|---|---|
bShr(value, shift) |
LuaBinary.pas:52 |
Right-shift: value >> shift. |
Loaded via luaL_openlibs at LuaHandler.pas:16185. Available: base,
string, table, math, io, os, coroutine, package, debug.
Functions used: string.char, string.byte, string.sub, string.format,
table.concat, ipairs, tonumber, tostring, pcall, loadstring,
error.
| Function | Source | Behavior |
|---|---|---|
createThread(func) |
LuaThread.pas:336-338 |
Creates a background thread running func(t). The thread gets its own coroutine of the main _LuaVM. The main script exits immediately. Used by ce_server.lua to keep CE UI responsive. |
t.Name |
TCEThread |
Thread name for debugging (shown in OutputDebugString). |
t.Terminated |
TCEThread |
Flag set by CE when the thread should stop (e.g., Lua Engine tab closed). Checked in server loops for clean shutdown. |
sleep(ms) |
Lua standard | Pauses the current thread. Used in retry loops. |
| Source | LuaHandler.pas:16199, impl readBytesEx lines 2508--2572 |
| Success | With true 3rd arg: table of bytes (1-indexed integers). |
| Failure | Returns nil. |
| Source | LuaHandler.pas:16200, impl writeBytesEx lines 2575--2652 |
| Success | Returns integer: bytes written. Server checks n > 0. |
| Failure | Returns 0. Note: 0 is truthy in Lua! |
| Source | LuaHandler.pas:16205, impl readQwordEx lines 1983--2015 |
| Success | Returns 8-byte value as integer. |
| Failure | Returns nil. |
| Source | LuaHandler.pas:16204, impl readIntegerEx lines 1930--1971 |
| Success | Returns 4-byte value as integer (unsigned by default). |
| Failure | Returns nil. |
| Source | LuaHandler.pas:16209, impl readStringEx lines 2126--2199 |
| Success | Returns null-terminated string. |
| Failure | Returns nil. |
| Source | LuaHandler.pas:16243, impl lines 4493--4540 |
| Success | Returns resolved address as integer. |
| Failure | Returns nil (no Lua error). |
Prefer over getAddress which raises a Lua error on failure (crashes server).
| Source | LuaHandler.pas:16317, impl lines 4753--4852 |
| Success | Returns table with Name, Address, Is64Bit, PathToFile. |
| Failure | Returns nil (e.g. no process attached). |
Length-prefixed binary framing:
[4-byte LE length][N bytes of UTF-8 text]
Responses use the same framing. Each client.py invocation opens a
fresh TCP connection (and consequently a fresh pipe connection), sends
one command, receives one response, and closes. Interactive mode (-i)
opens a new connection per command as well.
This repository does not ship a large private Lua investigation suite. Local
scratch scripts may live in gitignored helper/ — promote facts into
docs/game/<Title>/, not raw dumps.
Breakpoint-driven workflows (debugger_onBreakpoint, live register globals) do
not work over the current request/response pipe. See
docs/BREAKPOINT_STRATEGY.md and non-goals.
Prefer native commands when they exist (AOBScan, GroupScan, read*, al*, st*).
- Memory:
readByte/readBytes/readQword/readInteger/readFloat/write*/readString - Scanning (prefer native): remote
AOBScanandGroupScan— do not callcreateMemScan/varscan_*from the pipe server thread - Symbols / modules:
getAddressSafe,enumModules(or nativeenumModules/getAddress) - Process:
getOpenedProcessID(attach is normally done in the CE UI) - File I/O:
io.open/ write (works from background thread; still avoid huge dumps over the pipe)
Full early design notes (including unshipped debugger ideas): SOLUTION.md — historical only.
CE's createPipe creates pipes with PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT
(verified from luapipeserver.pas:71). This is byte mode, not message mode:
- Each
ReadFile/WriteFileoperates on a raw byte stream - No message boundaries — the client must use a length-prefixed protocol to know where responses end (which is what our relay does)
PIPE_WAIT= blocking/synchronous I/O (the relay must stay in blocking mode)
Our relay does not call SetNamedPipeHandleState at all — it opens the
pipe with CreateFileW and defaults to byte mode, which matches CE's pipe
mode. This is correct for the length-prefixed protocol.
| Symptom | Likely cause | Fix |
|---|---|---|
Failed to create named pipe |
CE can't create \\.\pipe\UEScanRemote |
Run CE as Administrator, or check no other instance owns the pipe. |
| No response from CE server | CE not running the Lua script, or relay not running | python client.py --cmd "ping" — expect "pong". |
| Connection refused (WSL) | Relay not started | Start windows_relay.py on Windows. |
| Connection refused (Windows) | Port conflict | Use --port to change port. Process list: netstat -ano | find :8888. |
CE freezes (only if running old synchronous ce_server.lua) |
Old version called main() on main thread |
Update to the current ce_server.lua which uses createThread for background execution. |
| Server responds to first command but hangs on reconnection | CE's CreateNamedPipe creates a single-instance pipe and acceptConnection doesn't call DisconnectNamedPipe before ConnectNamedPipe |
Already fixed in current ce_server.lua — destroys and recreates the pipe per client. |
| Relay: cannot connect to pipe after it worked | Lua pipe thread killed/hung by a prior client command (not “cold start flaky”) | Re-Execute ce_server.lua (v1.8.4 stops old thread + destroys pipe, then starts fresh). Also stop/restart windows_relay.py if it still holds a client connection. |
| “UEScanServer is already running” / cannot restart | Old message — pre-1.8.4 refused re-run; closing Lua console did not kill createThread |
Use v1.8.4+: re-Execute always restarts. Manual stop: _ue_stop_uescan_server("manual") in Lua Engine, or client shutdown. |
PowerShell Connect to UEScanRemote times out while thread Terminated |
Pipe already has a client (zombie relay) or thread stuck in ReadFile |
Kill windows_relay.py, then re-Execute ce_server.lua. |
| Timeout reading from pipe in relay | Client disconnected abruptly | No action needed — relay cleans up automatically. |
| CE server doesn't detect disconnection (pipe appears busy) | Pending ReadFile in relay's pipe_to_tcp thread holds kernel reference after CloseHandle |
Fixed in relay — uses CancelIoEx to cancel pending I/O across all threads before closing the handle. |
| cmd.exe / relay process “fully hung”, need Task Manager | Pre-fix: sync pipe ReadFile with no timeout while CE stuck/dead |
windows_relay.py: overlapped I/O + --timeout (default 300s), join caps, Ctrl+C on accept loop. Restart relay after CE re-Execute. |
python: command not found (Windows) |
Python not on PATH | Reinstall Python and check "Add Python to PATH". |
| WSL can't reach Windows | WSL2 network config | Use ip route | grep default to get Windows host IP, then python client.py --host <IP>. |
Canonical catalogue: docs/NONGOALS-AND-HAZARDS.md. Summary of common killers:
| Function / pattern | Why it crashes | Safe alternative |
|---|---|---|
enumMemoryRegions() |
Bad state / parser issues | enumModules + targeted ranges |
createMemScan / varscan_* from server/runScript |
Scan engine / UI thread issues | Native AOBScan or GroupScan (grouped, main-thread) |
AOBScan(..., "w", ...) bad prot |
Prot parse garbage | Native AOBScan or numeric/rwx prot only |
Long AOB via raw runScript |
Timeout / stuck server | Native AOBScan + high client timeout |
getStructure("Name") (string) |
Index 0 coercion / wipe risk | Native stFind / stGet |
| Empty global structure list | Dissect UI OOB | stEnsureSeed / keep DO_NOT_DELETE_PLACEHOLDER |
Rename structure mid-beginUpdate |
UI/rename fail | stEnd then stSetName |
Address list / Active without main-thread sync |
VCL crash | Native al* / st* only |
synchronize + CE modal (failed AA) |
Deadlock | aaCheck first; user present; timeout ≥120 |
Mass Active=true |
Inject storms / freezes | Enable one bootstrap at a time |
| Dumping all AA scripts one response | >48 KiB / hang | alDump metadata; chunk scripts |
UE/CE75 helpers (UEngine_*, G1R plugins) apply only when those scripts/plugins are loaded — not for Dying Light 2 by default.
When you must use runScript (no native command exists), follow these rules:
| Rule | Why | Example |
|---|---|---|
Always wrap in pcall |
CE functions can throw Lua errors that kill the background thread | local ok, r = pcall(readQword, addr) |
| Never iterate unbounded loops | A nil pointer in a linked-list walk crashes the thread | Set a max iteration limit (e.g., i < 100000) |
Guard against nil in string.format |
format("%X", nil) errors instead of producing "nil" |
local v = val or 0 before formatting |
| Prefer native commands over raw CE API | Server wrapper handles cleanup (e.g., list.destroy()) |
Use AOBScan not runScript AOBScan(...) |
| Check if a function exists before calling | Plugin functions may not be loaded | if type(UEngine_getAllProperties) == "function" then ... end |
| Verify object type before treating as UObject | A nil or non-UObject pointer has no Class at +0x10 | Check readQword(addr + 0x10) is non-zero before using |
- The relay binds to
127.0.0.1by default — only local connections. - No authentication or encryption. Anyone who can reach the TCP port can send arbitrary commands to CE.
- If you must access from another machine, use
--bind 0.0.0.0and restrict with Windows Firewall or SSH tunneling. - The
runScriptcommand allows arbitrary Lua execution in CE — equivalent to full memory read/write access to all attached processes.
Skills (skills/) teach how to operate the remote safely. They are not
per-game research dumps.
| Skill | Purpose | When |
|---|---|---|
ce-remote-scanning |
Safe scans, timeouts, crash avoidance | Any game |
ce-aob-scan |
AOB retune / ranking | Any game |
ce-table-remote |
al*/st* foundation (seed, rename-after-commit) |
Table work |
ce-table-migrate |
Port loaded CT (AA → enable → pointers → structs) | Table work |
dl2-table-work |
DL2 work order only (points at docs/game/DyingLight2/) |
Dying Light 2 |
ue-character-finding |
GEngine / CE75 character chain | UE games with CE75 only |
ue-stats-attributes |
GAS / attribute sets | UE + GAS only |
ue-inventory-hacking |
UE inventory component patterns | UE only |
Dying Light 2 is Techland (gamedll / engine), not GEngine/GAS. Do not
start with ue-* skills for DL2. Use:
- Skill:
skills/dl2-table-work(work order) - Knowledge: docs/game/DyingLight2/INDEX.md (status + topics)
- PlayerVariables: docs/game/DyingLight2/player-variables.md
- Tools:
ce-table-migrate,ce-aob-scan,ce-remote-scanning, docs/CE-GROUP-SCAN.md
| Doc | Role |
|---|---|
| docs/README.md | Documentation index |
| docs/TABLE-MIGRATE.md | Table rebind reference |
| docs/NONGOALS-AND-HAZARDS.md | Non-goals + crash catalogue |
| docs/CE-GROUP-SCAN.md | Grouped scan language + remote API |
| docs/CE75-INTEGRATION.md | UE / G1R + CE75 helpers (not DL2) |
| docs/UE-Memory-Patterns.md | UE5 patterns (G1R-based; not DL2) |
| docs/BREAKPOINT_STRATEGY.md | Why remote BP is not shipped |
| docs/CHANGELOG.md | Server / protocol version notes |
Cheat Engine is a product of Eric "Dark Byte" Heijnen (https://cheatengine.org). This project is an independent, third-party remote-control interface that happens to communicate with Cheat Engine's Lua scripting system. It has not been endorsed, reviewed, or approved by Eric Heijnen or any Cheat Engine contributors, and is not affiliated with the Cheat Engine project in any official capacity.
- Cheat Engine 7.5
- Windows: Python 3.6+ (no extra packages, uses stdlib + ctypes only)
- WSL/Linux: Python 3.6+ (stdlib only)
