Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.exeinD:\Gaming\Sacred Gold\(path inbuild\x86-debug\sacredK.ini,[paths] GameDir). Ignore theSacredReborn*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 markedTEMPORARY, 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 totools/names.txt(applied to Ghidra withtools/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. Keepdocs/todo.mdfor 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; definesfn_original),RE_HOOK_API,RE_STARTUP(name, fn)(runs after hooks are installed), patch helpers inpatch.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.cppis the HUD layer: 1024-wide windows are drawn shifted (Anchor::Center/Right, orScreenfor windows laid out across the full width), mouse events and the cursor shifted to match; other mods usehud::RenderCentred/HandleCentred/DrawOffsetXinstead of hooking the same function again.docs/— notes per topic;function-map.mdlists known functions.
Reverse-engineering workflow
- Decompile:
.\tools\decompile.ps1 0xADDR [...] -Out ghidra\x.c -ReadOnly(persistent Ghidra project inghidra/; large functions can time out — fall back to disassembly). - Disassemble:
objdump -d --no-show-raw-insn --start-address=.. --stop-address=..onsacred.exe(Git Bash has mingw objdump), ortools/disasm.mjs. A full.textdump grepped for immediates/calls is often the fastest way to find all users of a constant or function;tools/find-imm.mjsdoes the same. - File offset ↔ VA:
.textVA = offset + 0x400000;.rdata0x88e000 ↔ 0x48e000;.data0x8e5000 ↔ 0x4e5000. Vtables via RTTI (tools/rtti.mjs,Class::vfNNnames 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 withRedirectCall/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.
GetKeyStateis 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 constructormarks 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.