Files
sacred_k/CLAUDE.md
T
2026-10-19 22:13:36 +02:00

7.9 KiB

CLAUDE.md

Guidance for agents working in this repository. Read README.md (what the project is, build, run, layout) and tools/README.md (RE tools, Ghidra project, autotest) first; this file adds how we work and what we learned.

What this is

sacredK.dll is injected into the original Sacred Gold 2.28 sacred.exe (2006, 32-bit, MSVC 6) by the launcher sacredK.exe. It hooks and patches the running game: some functions are rewritten, most of the work so far is mods — behaviour changes on top of the original (widescreen, HUD layout, 7 weapon sets / art slots, key bindings, WASD, auto-attack, skill reworks, respec, map fixes, ...). The game stays playable at every step; the user plays it to test.

  • Target binary: the original sacred.exe in D:\Gaming\Sacred Gold\ (path in build\x86-debug\sacredK.ini, [paths] GameDir). Ignore the SacredReborn* files in that folder.
  • Game texts are German (Fertigkeiten, Attribute, Blitzschlag, ...); keep the game's names when talking about features.
  • The user's monitor is 3440x1440; the game renders about 1835x768 scaled up. Widescreen goal: same on-screen size as the original, view extended horizontally only. Only 16:9 and wider are supported (no 4:3).

How the user works (important)

  • The user tests in-game and reports back ("1: works, 2: ..."). After a change, say exactly what to test, numbered. Don't claim something works until the user (or a test you ran) confirmed it.
  • Their game start builds the DLL. A link error LNK1168 (DLL locked) means the game is running; that's expected, not a failure to fix.
  • Ask before running the autotest — it takes over mouse, keyboard and screen. Keep tests fast (log markers, not sleeps). Save row 2 (-Save 2) is the battlemage, the usual test character.
  • Commit only what the user confirmed, or when asked. End commit messages with the attribution line given by the harness.
  • Feature requests often arrive mid-task as change-request lists; note them, finish the piece in hand, then take them in order.

Principles

  • Measure before imitating. Log the original's runtime behaviour (orders, fields, distances, draw calls) instead of guessing from decompiles. Many wrong turns in this project came from trusting a decompile; most fixes came from a one-off log. Temporary diagnostics: log lines start with diag, code is marked TEMPORARY, and both are removed before committing.
  • No vanilla preservation. Once a mod works, delete the old path, toggles and fallbacks. Keep only migrations (e.g. old save formats).
  • Refactor duplicates while writing. Repeated operations go into shared headers (src/game/engine/*.h, src/game/ui/*.h) or tables (re::PatchAll, re::ReplaceAll), not copies.
  • Name as you go. Functions, globals (g_), fields and constants get readable names when identified; add function names to tools/names.txt (applied to Ghidra with tools/rename.ps1). No bare hex in logic: named constants with a comment saying where they come from.
  • Data-driven over id lists. Prefer the game's own data (art definition tables, weapon class, launch type, the game's own behaviour) over per-art id constants. Hard-coded art ids don't scale.
  • Document each mod in docs/<topic>.md: the request (with date), what was measured, addresses, how the mod works. Keep docs/todo.md for noted but deferred requests.

Build and run

.\build.ps1                                   # x86-debug -> build\x86-debug\
build\x86-debug\sacredK.exe log             # start the game with the DLL

New source files must be added to CMakeLists.txt. Output to check: build\x86-debug\sacredK.log (written anew each start; our lines plus the game's own log). Crashes: sacredK_crash.txt / .dmp next to the DLL (exception, module+offset, ebp chain); Windows also records the faulting offset in the Application event log (Get-WinEvent, provider "Application Error"). The debug build's run-time checks turn calling-convention mistakes into a breakpoint exception (0x80000003, "ESP was not properly saved").

Code map

  • src/re/ — framework: RE_HOOK(address, fn) (Detours; defines fn_original), RE_HOOK_API, RE_STARTUP(name, fn) (runs after hooks are installed), patch helpers in patch.h (PatchInstructionDword, PatchAll, RedirectCall, RedirectJcc, RedirectOperand, ReplaceCode/ReplaceAll; all verify the original bytes first), config, log, crash handler.
  • src/game/ — the game's structures and functions by subsystem, as inline wrappers with the address and what is known: engine/orders.h (creature orders, OnCreatureOrder, creature fields and actions, IsHostileTo, DistanceTo, ContactDistance, WeaponClass, NearestHostile), engine/arts.h (art definitions, cooldowns, launch types), engine/objects.h (object manager, nearby creatures, player hero), engine/fx.h (FX objects and events), engine/events.h (engine tick, save loaded), ui/ (UI rects, fonts, events), addresses.h (shared tables).
  • src/mods/ — one file per mod; the top comment explains the request, the measured behaviour and the approach. hud.cpp is the HUD layer: 1024-wide windows are drawn shifted (Anchor::Center/Right, or Screen for windows laid out across the full width), mouse events and the cursor shifted to match; other mods use hud::RenderCentred / HandleCentred / DrawOffsetX instead of hooking the same function again.
  • docs/ — notes per topic; function-map.md lists known functions.

Reverse-engineering workflow

  • Decompile: .\tools\decompile.ps1 0xADDR [...] -Out ghidra\x.c -ReadOnly (persistent Ghidra project in ghidra/; large functions can time out — fall back to disassembly).
  • Disassemble: objdump -d --no-show-raw-insn --start-address=.. --stop-address=.. on sacred.exe (Git Bash has mingw objdump), or tools/disasm.mjs. A full .text dump grepped for immediates/calls is often the fastest way to find all users of a constant or function; tools/find-imm.mjs does the same.
  • File offset ↔ VA: .text VA = offset + 0x400000; .rdata 0x88e000 ↔ 0x48e000; .data 0x8e5000 ↔ 0x4e5000. Vtables via RTTI (tools/rtti.mjs, Class::vfNN names in Ghidra).
  • Verify calling conventions before hooking or calling: check the function's ret N (callee-popped bytes) and every argument, including structs passed by value. A wrong count corrupts the caller's stack. Use __fastcall(self, edx, ...) as the stand-in for __thiscall.
  • Register-passing call sites are handled with naked thunks (__declspec(naked), push the registers, call C++, ret N) installed with RedirectCall/RedirectJcc.

Pitfalls learned

  • Two RE_HOOKs on the same address don't stack; extend the existing hook (or expose a helper from the mod that owns it).
  • UI windows are laid out for 1024x768 and render even while hidden; check a control's visible flag (+0x10 bit 0) before drawing extras.
  • The hero's attributes/skills (cCreatureArts, hero +0x3a8) are mirrored xor-ed into a protected page (0x182cddc); change them only through the game's own functions (they refresh the copy), never by writing fields.
  • GetKeyState is per-thread message state: Alt reads as held after Alt+Tab until pressed again in the game. Don't gate key handling on it.
  • Hard-coded 1024/768, 512/384, 0x3ff/0x2ff constants appear throughout the UI code; widening a window means finding all of them (see docs/hud.md, docs/widescreen.md). Shared drawing routines (e.g. the map's zoomed view) may serve windows with different offsets.
  • The game's log messages ([game] ... in our log) are useful landmarks, e.g. cEngine End and exit constructor marks a finished save-game load.

Memory

Project memory lives in C:\Users\torha\.claude\projects\D--Projekte-sacred-k\memory\ (project-status.md: what is done and confirmed, what is open). Update it after commits and before long pauses.