From 52db0f79f90c96c12d41a0825f0cc94c4bb37e8b Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 14 Oct 2026 09:08:52 +0200 Subject: [PATCH] tools: function names for the Ghidra project, Hold-Key test helper - tools/names.txt lists identified sacred.exe functions ("0xADDR Class::Method"); rename.ps1 / Rename.java apply them to the persistent Ghidra project, so decompiles read with real names. - decompile.ps1: parameters other than the addresses are named-only (the first address was taken as -Out). - game-input.ps1: Hold-Key for polled keys such as movement. Co-Authored-By: Claude Opus 5.5 (1M context) --- tools/README.md | 3 +- tools/decompile.ps1 | 1 + tools/game-input.ps1 | 9 +++++ tools/ghidra/Rename.java | 74 +++++++++++++++++++++++++++++++++++++ tools/names.txt | 80 ++++++++++++++++++++++++++++++++++++++++ tools/rename.ps1 | 16 ++++++++ 6 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 tools/ghidra/Rename.java create mode 100644 tools/names.txt create mode 100644 tools/rename.ps1 diff --git a/tools/README.md b/tools/README.md index 43f8989..45c6997 100644 --- a/tools/README.md +++ b/tools/README.md @@ -10,6 +10,7 @@ passed explicitly. | `find-imm.mjs /regex/` | every instruction whose text matches (e.g. `/96AB0h\]/`) | | `rtti.mjs ` | vtables of an MSVC class via RTTI | | `decompile.ps1 ` | decompile from the persistent Ghidra project `ghidra/` | +| `rename.ps1` | apply the function names in `names.txt` to the Ghidra project (add names there as functions are identified) | ## Persistent Ghidra project (`ghidra/`, not in git) @@ -36,5 +37,5 @@ settings to `build\x86-debug\sacred_re.ini` (restored afterwards), e.g. ``` Click positions assume the classic menu layout scaled onto a 3440x1440 -monitor. `game-input.ps1` has the building blocks (start, click, key, +monitor. `game-input.ps1` has the building blocks (start, click, key, hold key, screenshot). Don't touch mouse/keyboard while a test runs. diff --git a/tools/decompile.ps1 b/tools/decompile.ps1 index bf2fad2..5c5aaf1 100644 --- a/tools/decompile.ps1 +++ b/tools/decompile.ps1 @@ -2,6 +2,7 @@ # .\tools\decompile.ps1 0x6322b0 0x62ae70 [-Out file.c] [-ReadOnly] # Prints the C to stdout (and keeps it in -Out, default ghidra\last.c). # Without -ReadOnly, functions created for vtable-only methods are saved. +[CmdletBinding(PositionalBinding = $false)] param( [Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$Addresses, [string]$Out = "", diff --git a/tools/game-input.ps1 b/tools/game-input.ps1 index bf655ae..a7cf4ca 100644 --- a/tools/game-input.ps1 +++ b/tools/game-input.ps1 @@ -4,6 +4,7 @@ # Start-Sacred # launcher, returns the sacred.exe process # Click-At 1720 779 # screen pixels (left click) # Press-Key 0x1B # virtual key (ESC) +# Hold-Key 0x27 -Ms 1500 # hold a key (right arrow) # Save-Screen screenshots\x.png # primary monitor area of the game # Stop-Sacred $game # only the given process # @@ -85,3 +86,11 @@ function Move-To([int]$X, [int]$Y, [int]$AfterMs = 800) { } Start-Sleep -Milliseconds $AfterMs } + +# Holds a key down for a while (polled keys such as movement need this). +function Hold-Key([byte]$VirtualKey, [int]$Ms = 1500, [int]$AfterMs = 500) { + [SacredRe.Input]::keybd_event($VirtualKey, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds $Ms + [SacredRe.Input]::keybd_event($VirtualKey, 0, 2, [UIntPtr]::Zero) + Start-Sleep -Milliseconds $AfterMs +} diff --git a/tools/ghidra/Rename.java b/tools/ghidra/Rename.java new file mode 100644 index 0000000..5ce6a7e --- /dev/null +++ b/tools/ghidra/Rename.java @@ -0,0 +1,74 @@ +// Apply function names from tools/names.txt to the persistent sacred.exe +// Ghidra project. +// +// Headless usage (see tools/rename.ps1): +// analyzeHeadless sacred -process sacred.exe -noanalysis +// -scriptPath tools/ghidra -postScript Rename.java +// +// names.txt: one "0xADDRESS Name" per line, "Class::Method" puts the function +// into a class namespace; "#" starts a comment. Functions are created where +// none starts at the address yet. +// @category sacred_re + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +import ghidra.app.cmd.disassemble.DisassembleCommand; +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.Address; +import ghidra.program.model.listing.Function; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.symbol.SymbolTable; + +public class Rename extends GhidraScript { + + @Override + protected void run() throws Exception { + String[] args = getScriptArgs(); + if (args.length != 1) { + printerr("usage: Rename.java "); + return; + } + SymbolTable symbols = currentProgram.getSymbolTable(); + int renamed = 0; + for (String raw : Files.readAllLines(Paths.get(args[0]), StandardCharsets.UTF_8)) { + String line = raw.replaceAll("#.*", "").trim(); + if (line.isEmpty()) { + continue; + } + String[] parts = line.split("\\s+"); + if (parts.length != 2) { + printerr("bad line: " + raw); + continue; + } + Address address = toAddr(parts[0]); + Function function = getFunctionAt(address); + if (function == null) { + new DisassembleCommand(address, null, true).applyTo(currentProgram, monitor); + function = createFunction(address, null); + } + if (function == null) { + printerr("no function at " + parts[0]); + continue; + } + String name = parts[1]; + Namespace namespace = currentProgram.getGlobalNamespace(); + int separator = name.lastIndexOf("::"); + if (separator >= 0) { + String className = name.substring(0, separator); + name = name.substring(separator + 2); + namespace = symbols.getNamespace(className, currentProgram.getGlobalNamespace()); + if (namespace == null) { + namespace = symbols.createClass(currentProgram.getGlobalNamespace(), className, + SourceType.USER_DEFINED); + } + } + function.setParentNamespace(namespace); + function.setName(name, SourceType.USER_DEFINED); + renamed++; + } + println("renamed " + renamed + " functions"); + } +} diff --git a/tools/names.txt b/tools/names.txt new file mode 100644 index 0000000..c3477ae --- /dev/null +++ b/tools/names.txt @@ -0,0 +1,80 @@ +# Function names for sacred.exe (Sacred Gold 2.28), applied to the Ghidra +# project by tools/rename.ps1. One "0xADDRESS Name" per line; "Class::Method" +# names go into a class namespace. Keep in sync with src/game/addresses.h. + +# --- startup / engine core --------------------------------------------------- +0x0066f1c0 Log +0x00813140 CmdLineHasFlag +0x0066e500 GetMouseClientPos +0x008122a0 MainWindowProc +0x00849701 __RTDynamicCast +0x0084aa86 ToUpper + +# --- renderer ---------------------------------------------------------------- +0x00645390 DxDriver7::SetDisplayMode +0x00645f70 DxDriver7::Flip +0x00646f50 DxDriver7::BeginScene +0x00646fd0 DxDriver7::EndScene +0x006469d0 DxDriver7::LoadingScreen + +# --- kernel event queue ------------------------------------------------------ +0x00808490 Kernel_Instance +0x00808930 cKernel::PostEvent # (event, flags, delayMs); key events (category 4) are queued +0x008087b0 cKernel::DispatchQueued + +# --- engine (cEngine, vtable 0x88ff34) --------------------------------------- +0x0060e3f0 cEngine::RenderThreadLoop +0x0060f1d0 cEngine::Tick # per game tick (10 Hz): queued events, then hold-to-move orders +0x00618130 cEngine::ReceiveEvent # vf00: offers events to the UI, then engine hotkeys (A, P, Q/W/E/R/B/Space, ^) +0x00617360 cEngine::HandleMouseButton # click: pick object / walk to ground point +0x006170d0 cEngine::SendCreatureOrder # (creature, cEvent_creature*): snaps ground targets, then creature->vf18 +0x00611f10 cEngine::UpdateCursorPick # per frame: object under the cursor +0x00610650 cEngine::ScrollInput # arrow keys: walk object #2 or scroll the camera +0x00613040 cEngine::UseActionSlot # hotkeys Q/W/E/R/B/Space +0x00613710 cEngine::CaptureScreen # save-game thumbnail + +# --- objects / creatures ----------------------------------------------------- +0x005fe060 cObjectManager::GetObject # (id); manager at *0xad3bc0 +0x00603e90 cObjectManager::GetPlayerHero +0x00546ed0 cCreature::IsWalking # state +0x150 == 1 +0x0052e400 cCreature::ReceiveOrder # vf18 +0x004fb620 cCreature::TraceToCursor # line trace from the creature to the cursor's world point (aiming) + +# --- mouse cursor (single instance) ------------------------------------------ +0x006550f0 MouseCursor_Instance +0x00654e10 cMouseCursor::cMouseCursor +0x00655670 cMouseCursor::SetPosition +0x006559e0 cMouseCursor::GetX +0x006559f0 cMouseCursor::GetY +0x006559a0 cMouseCursor::GetPreviousPosition +0x006555e0 cMouseCursor::DrawItem +0x00655ab0 cMouseCursor::IsButtonHeld + +# --- world view (cWorldView0, vtable 0x8900f8) ------------------------------- +0x00624ae0 cWorldView0::SetupView +0x00628210 cWorldView0::SetZoom +0x006322b0 cWorldView0::Render +0x0062a730 cWorldView0::UpdateCamera +0x0062b000 cWorldView0::RenderTileRow # tiles + collects objects into the draw lists +0x0062e580 cWorldView0::DrawObjects +0x006300d0 cWorldView0::DrawObjects2 +0x00623c40 cWorldView0::WorldToScreen +0x00623a20 cWorldView0::ScreenToView +0x00626d30 cWorldView0::PickObjectAt # (screen x, y) -> object id +0x00627260 cWorldView0::DrawObjectLabels # Alt +0x006426a0 BuildOrthoMatrix + +# --- UI ---------------------------------------------------------------------- +0x00756820 UiManager_Instance # *0x17ecb3c +0x00756940 cUI_Manager::ThreadLoop +0x00757120 cUI_Manager::ReceiveEvent # vf00: UI hotkeys (C F H I L M N O S T) +0x007587b0 cUI_Manager::Render +0x0075a860 cUI_Manager::GetFocus +0x0075a370 cUI_Manager::IsUiAtPoint +0x0075bbf0 cUI_Manager::DrawStatusLine +0x0075c200 cUI_Manager::DrawMessage +0x00731d40 cUI_Window2::HitTest +0x006dfb10 cUI_Taskbar2::HitTest +0x006e1970 cUI_Taskbar2::HandleEvent # digit keys: belt / skill slots +0x006e3ac0 cUI_Taskbar2::Render +0x006e7300 cUI_Tooltip::Render diff --git a/tools/rename.ps1 b/tools/rename.ps1 new file mode 100644 index 0000000..299492f --- /dev/null +++ b/tools/rename.ps1 @@ -0,0 +1,16 @@ +# Apply tools/names.txt to the persistent Ghidra project (ghidra/sacred.gpr). +# .\tools\rename.ps1 +$ErrorActionPreference = "Stop" +$root = Split-Path $PSScriptRoot +$ghidra = if ($env:GHIDRA_INSTALL_DIR) { $env:GHIDRA_INSTALL_DIR } else { "E:\Coding\Ghidra\ghidra_12.1.4_PUBLIC" } +if (-not $env:JAVA_HOME) { $env:JAVA_HOME = "C:\Program Files\Java\jdk-21" } + +$arguments = @( + (Join-Path $root "ghidra"), "sacred", + "-process", "sacred.exe", "-noanalysis", + "-scriptPath", (Join-Path $PSScriptRoot "ghidra"), + "-postScript", "Rename.java", (Join-Path $PSScriptRoot "names.txt") +) +& (Join-Path $ghidra "support\analyzeHeadless.bat") @arguments *> (Join-Path $root "ghidra\rename.out") +if ($LASTEXITCODE -ne 0) { Get-Content (Join-Path $root "ghidra\rename.out") -Tail 30; exit $LASTEXITCODE } +Select-String -Path (Join-Path $root "ghidra\rename.out") -Pattern "renamed|bad line|no function"