Skip to content

Latest commit

 

History

History
242 lines (183 loc) · 11.6 KB

File metadata and controls

242 lines (183 loc) · 11.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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.

Build Commands

Two build methods are available. Both require VS2026.

Method 1: CMake + Ninja (CLI, fast incremental builds)

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' debug

From 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 debug

build.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/).

Method 2: Visual Studio IDE (Solution/Project)

# Generate .sln and .vcxproj files:
D:\Github\CE-Handwire\gen_sln.bat

# Then open in VS2026:
# build\vs\CE-Handwire.sln

The 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.

Auto-deploy to CE

Set CE_PLUGINS_DIR cache variable to auto-copy DLL after build:

cmake --preset x64-debug -DCE_PLUGINS_DIR="C:/path/to/CheatEngine/plugins"

Build Environment

  • 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-

External Dependencies (local repos)

These repos must be cloned as siblings to CE-Handwire:

  • D:\Github\Zydis — x86/x64 instruction decoder (added via add_subdirectory)
  • D:\Github\imgui — UI toolkit (planned, not yet integrated)
  • D:\Github\cheat-engine — CE source (reference only, SDK header copied to sdk/)
  • Lua 5.3 — CE ships lua53-64.dll + lua53-64.lib in its plugins/ folder; linked at build time for GetLuaState() + luaL_dostring() calls

Repository Structure

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

Architecture

Module System

Each feature implements IModule (defined in src/core/module_registry.h):

  • init(pluginId) — register plugin types with CE
  • shutdown() — unregister callbacks, destroy UI
  • Modules are initialized in order, shut down in reverse order
  • A failing module is logged and skipped (graceful degradation)

Threading Model

  • 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).

Logging

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 OutputDebugStringA for VS Output window
  • Log files: next to the DLL

SDK Helpers (src/core/sdk_helpers.h)

  • sdk::ReadMem(addr, buf, size) — safe double-deref ReadProcessMemory wrapper
  • sdk::WriteMem(addr, buf, size) — safe double-deref WriteProcessMemory wrapper
  • sdk::Read<T>(addr)std::optional<T> — typed memory read
  • sdk::Deref(addr)std::optional<UINT_PTR> — pointer dereference
  • sdk::str(literal)const_cast<char*> for SDK's non-const char* params
  • sdk::fmtFloat(buf, size, val, precision) — CE-style decimal formatting with trailing zero trim

Plugin Architecture

Three Required DLL Exports

CEPlugin_GetVersion(PPluginVersion pv, int sizeofpluginversion)
CEPlugin_InitializePlugin(PExportedFunctions ef, int pluginid)
CEPlugin_DisablePlugin(void)

Plugin Types (registered via RegisterFunction)

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

Critical SDK Pitfalls (Must-Know)

  1. GetVersion second param is int, NOT int* — dereferencing causes AV at address 0x10
  2. pluginname must be static/global — stack pointer becomes dangling after GetVersion returns
  3. ReadProcessMemory / WriteProcessMemory are double pointers — call with (*g_Exported.ReadProcessMemory)(...)
  4. previousOpcode / nextOpcode return absolute addresses, not deltas — SDK header incorrectly declares DWORD return type; our sdk/cepluginsdk.h fixes this to UINT_PTR
  5. ExportedFunctions struct layout is sacred — never reorder, remove, or add fields; misalignment crashes all function pointer calls
  6. SDK uses char* not const char* — use sdk::str() or const_cast<char*>()
  7. ptDisassemblerContext is value 6, not 5 — value 5 is ptMainMenu
  8. Lua header stubs required — original SDK includes lua.h; our sdk/lua_stubs.h provides minimal stubs

Shell Command Notes (Windows 11)

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 -10

vswhere — find VS installation:

'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe' -latest -property installationPath

VS2026 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

Implemented Features

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

Abandoned Features

ValueClamp (removed)

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.

AccessLogger (removed)

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 ExceptionAddress in the DEBUG_EVENT before 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 — Lua debug_getContext() polling never catches the break.
  • Lua form-scraping of TFoundCodeDialog was attempted but the form class was not discoverable via CE's Lua getForm() 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.

Test Target (CE-Handwire-TestTarget.exe)

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 traceable mov [reg+offset], addss xmm, direct module-relative access instructions
  • Static CRT: all traces stay within the EXE module (no vcruntime DLL noise)