This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CE-Handwire is a native C++ plugin for Cheat Engine (CE) 7.x that enhances memory debugging workflows. It runs through the CE Plugin SDK (Version 6) for maximum performance, unlike Lua-based scripts. The plugin is compiled as a DLL (CE-Handwire.dll) placed in CE's plugins folder.
Two build methods are available. Both require VS2026.
From Claude Code (Bash tool) — use build.ps1 directly for clean UTF-8 output:
# Both configs (debug + release):
powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'D:/Github/CE-Handwire/build.ps1'
# Debug only (faster, preferred during iteration):
powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'D:/Github/CE-Handwire/build.ps1' debugFrom a human terminal:
:: cmd.exe — build.bat delegates to build.ps1, supports same args:
build.bat & :: debug + release
build.bat debug & :: debug only
build.bat release & :: release only
:: PowerShell terminal:
.\build.ps1 debugbuild.ps1 sources vcvarsall.bat amd64 silently, then calls cmake directly in the
PowerShell process — no cmd.exe wrapper that would garble output through the Bash tool.
Output: build/x64-debug/CE-Handwire.dll + CE-Handwire-TestTarget.exe (or build/x64-release/).
Output: build/x64-debug/CE-Handwire.dll + CE-Handwire-TestTarget.exe (or build/x64-release/).
# Generate .sln and .vcxproj files:
D:\Github\CE-Handwire\gen_sln.bat
# Then open in VS2026:
# build\vs\CE-Handwire.slnThe gen_sln.bat script generates a VS2026 solution at build/vs/CE-Handwire.sln with all projects (CE-Handwire, Zydis, Zycore). Build and debug directly from the IDE. Re-run gen_sln.bat after modifying CMakeLists.txt.
Set CE_PLUGINS_DIR cache variable to auto-copy DLL after build:
cmake --preset x64-debug -DCE_PLUGINS_DIR="C:/path/to/CheatEngine/plugins"- IDE: Visual Studio 2026 (v18, MSVC 19.50)
- C++ Standard: C++23
- Compiler: MSVC v145 (cl.exe)
- Build: CMake 3.25+ with Ninja generator
- Output: Statically-linked DLL (static CRT,
/MT//MTd) - Flags:
/utf-8 /W4 /permissive-
These repos must be cloned as siblings to CE-Handwire:
D:\Github\Zydis— x86/x64 instruction decoder (added viaadd_subdirectory)D:\Github\imgui— UI toolkit (planned, not yet integrated)D:\Github\cheat-engine— CE source (reference only, SDK header copied tosdk/)- Lua 5.3 — CE ships
lua53-64.dll+lua53-64.libin itsplugins/folder; linked at build time forGetLuaState()+luaL_dostring()calls
src/
main.cpp # DllMain + 3 CE plugin exports + module init
core/
globals.h # g_Exported, g_PluginId, g_PluginName (inline globals)
config.h # Centralized constants for all modules (tunable)
module_registry.h / .cpp # IModule interface + init/shutdown lifecycle
sdk_helpers.h # ReadMem, WriteMem, Read<T>, Deref wrappers
logging/
log.h / .cpp # Lock-free MPSC ring buffer logger
features/ # One subdirectory per feature module
sdk/
cepluginsdk.h # Modified CE SDK header (bug fixes applied)
lua_stubs.h # Lua 5.3 header redirect (CE's lua53-64.dll)
tools/
test_target/
main.cpp # Standalone EXE for testing plugin features in CE
docs/private/ # Private submodule with SDK reference docs
Each feature implements IModule (defined in src/core/module_registry.h):
init(pluginId)— register plugin types with CEshutdown()— unregister callbacks, destroy UI- Modules are initialized in order, shut down in reverse order
- A failing module is logged and skipped (graceful degradation)
- Main thread: All UI callbacks, timer callbacks, SDK function calls, Lua calls
- Debugger thread: Type 2 callbacks only — NEVER call SDK functions (deadlock)
- Log writer thread: Async file I/O only — never calls SDK
- Pattern: Timer-driven polling on main thread. PointerInspector uses Lua
debug_getContext()on timer to read registers (avoids Type 2 deadlock/wrong-context issues).
LOG_DBG/INFO/WARN/ERR(module, fmt, ...) — lock-free, never blocks caller.
- MPSC ring buffer (8192 slots) with dedicated writer thread
- Readable timestamps:
[HH:MM:SS.mmm][INF][T1234][Module] message - File size control: max 5MB per file, dual-file ping-pong (
CE-Handwire.log/CE-Handwire.log.1) - Debug builds also emit
OutputDebugStringAfor VS Output window - Log files: next to the DLL
sdk::ReadMem(addr, buf, size)— safe double-deref ReadProcessMemory wrappersdk::WriteMem(addr, buf, size)— safe double-deref WriteProcessMemory wrappersdk::Read<T>(addr)→std::optional<T>— typed memory readsdk::Deref(addr)→std::optional<UINT_PTR>— pointer dereferencesdk::str(literal)—const_cast<char*>for SDK's non-const char* paramssdk::fmtFloat(buf, size, val, precision)— CE-style decimal formatting with trailing zero trim
CEPlugin_GetVersion(PPluginVersion pv, int sizeofpluginversion)
CEPlugin_InitializePlugin(PExportedFunctions ef, int pluginid)
CEPlugin_DisablePlugin(void)
| Value | Type | Thread | Notes |
|---|---|---|---|
| 0 | Address List context menu | Main | |
| 1 | Memory Browser menu | Main | |
| 2 | Debug event handler | Debugger thread | Do NOT call exported functions — deadlock risk |
| 3 | Process watcher | Main | |
| 5 | Main form menu | Main | Often confused with Type 6 |
| 6 | Disassembler context menu | Main | |
| 7 | Disassembler line renderer | Main | Must complete in <1ms |
| 8 | AutoAssembler hook | Main |
GetVersionsecond param isint, NOTint*— dereferencing causes AV at address 0x10pluginnamemust be static/global — stack pointer becomes dangling afterGetVersionreturnsReadProcessMemory/WriteProcessMemoryare double pointers — call with(*g_Exported.ReadProcessMemory)(...)previousOpcode/nextOpcodereturn absolute addresses, not deltas — SDK header incorrectly declaresDWORDreturn type; oursdk/cepluginsdk.hfixes this toUINT_PTRExportedFunctionsstruct layout is sacred — never reorder, remove, or add fields; misalignment crashes all function pointer calls- SDK uses
char*notconst char*— usesdk::str()orconst_cast<char*>() ptDisassemblerContextis value 6, not 5 — value 5 isptMainMenu- Lua header stubs required — original SDK includes
lua.h; oursdk/lua_stubs.hprovides minimal stubs
This project runs on Windows. Bash tool uses Git Bash (/usr/bin/bash). Key rules to avoid repeated errors:
Paths with spaces — always use single quotes in bash:
# Correct:
ls 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\'
# Wrong (double quotes break on backslashes in bash):
ls "C:\Program Files\..."Running build from bash — call build.ps1 directly, never wrap cmd.exe:
# Correct — clean UTF-8, no garbled Chinese error text:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'D:/Github/CE-Handwire/build.ps1' debug
# Wrong — cmd.exe stderr gets wrapped into localized PS ErrorRecord (garbled):
powershell.exe -NoProfile -Command "& {& cmd.exe /C 'D:\Github\CE-Handwire\build.bat' 2>&1}"Git commands — work normally in bash:
git status
git diff
git log --oneline -10vswhere — find VS installation:
'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -property installationPathVS2026 paths (confirmed):
- Install:
C:\Program Files\Microsoft Visual Studio\18\Community - MSVC:
.../VC/Tools/MSVC/14.50.35717/bin/Hostx64/x64/cl.exe - CMake:
.../Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe - Ninja:
.../Common7/IDE/CommonExtensions/Microsoft/CMake/Ninja/ninja.exe - vcvarsall:
.../VC/Auxiliary/Build/vcvarsall.bat
| Module | Plugin Types | Description |
|---|---|---|
| BatchRename | Type 5 + Lua | Scan-all batch rename via Lua (filter, ignore-case, zero-pad) |
| MemoryBookmarks | Type 1 + Type 6 + Timer | Per-module bookmarks with LRU eviction, timer-driven active module detection |
| PointerInspector | Type 1 + Timer + Lua | Lua-based register capture via debug_getContext() + deref display |
| RegisterGoto | Type 1 + Type 6 | Navigate disassembler to register values with back-stack |
| OperandTracker | Type 6 + Type 7 | Zydis-based memory operand tracker, displayed in PointerInspector UI |
Goal: periodic min/max clamping of memory record values via timer-driven polling (Type 0 + Timer). Removed because PLUGINTYPE0_RECORD->address captures the resolved address at callback time as a fixed UINT_PTR, but CE memory records can use pointer chains whose resolved address changes at runtime. The stored address becomes stale, causing the clamp to read/write wrong memory or silently fail. Switching to Lua-based address resolution (via memoryrecord.CurrentAddress) was considered but rejected due to description-based record lookup not being uniquely reliable and the feature overlapping significantly with CE's built-in Freeze functionality.
Goal: capture EA + memory values at the instant a debug breakpoint fires (CE's "Find out what accesses this address"). Removed after 5 failed attempts — the CE Plugin SDK does not provide usable register context or instruction addresses in Type 2 callbacks:
- CE zeroes
ExceptionAddressin theDEBUG_EVENTbefore calling plugin Type 2 callbacks. GetThreadContext()on the debugger thread returns stale context (CE has already resumed the thread).- CE's auto-resume breakpoints mean
debug_isBroken()is always false — Luadebug_getContext()polling never catches the break. - Lua form-scraping of
TFoundCodeDialogwas attempted but the form class was not discoverable via CE's LuagetForm()API.
Future avenue: CE internally stores register context in TCodeRecord.context (see FoundCodeUnit.pas). If CE exposes this via Lua properties, or if the Delphi object layout can be reverse-engineered for raw memory reads, this feature becomes feasible. Lua-based approaches are likely too slow for production use; a native solution reading CE's internal structures would be preferred.
Standalone console EXE for testing plugin features without a real game. Built alongside the DLL.
- AOB signature:
43 45 54 45 53 54 00 00("CETEST") — scan in CE to locate the variable block - Variables: byte, word, dword, udword, qword, float, double, sbyte, sword — each 8 bytes apart
- Keys:
[M]mutate once,[A]auto-mutate (~500ms toggle),[Q]quit - Memory access patterns:
__declspec(noinline)functions generate traceablemov [reg+offset],addss xmm, direct module-relative access instructions - Static CRT: all traces stay within the EXE module (no vcruntime DLL noise)