Add hook debug tooling and refine RT3 atlas

This commit is contained in:
Jan Petykiewicz 2026-04-08 16:31:33 -07:00
commit 57bf0666e0
38 changed files with 14437 additions and 873 deletions

View file

@ -0,0 +1,260 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import re
from collections import deque
from dataclasses import dataclass
from pathlib import Path
ADDRESS_RE = re.compile(r"0x[0-9a-fA-F]{8}")
@dataclass(frozen=True)
class Row:
address: int
size: str
name: str
subsystem: str
calling_convention: str
prototype_status: str
source_tool: str
confidence: str
notes: str
verified_against: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Export a bounded function subgraph from function-map.csv notes."
)
parser.add_argument("function_map", type=Path)
parser.add_argument("output_prefix", type=Path)
parser.add_argument(
"--seed",
action="append",
default=[],
help="Seed function address in hex. May be repeated.",
)
parser.add_argument(
"--depth",
type=int,
default=2,
help="Traversal depth over note-address references.",
)
parser.add_argument(
"--title",
default="Function Subgraph",
help="Title used in the markdown summary.",
)
parser.add_argument(
"--include-backrefs",
action="store_true",
help="Also traverse rows that reference currently included nodes.",
)
args = parser.parse_args()
if not args.seed:
parser.error("at least one --seed is required")
return args
def parse_hex(text: str) -> int:
value = text.strip().lower()
if value.startswith("0x"):
value = value[2:]
return int(value, 16)
def fmt_addr(value: int) -> str:
return f"0x{value:08x}"
def load_rows(path: Path) -> dict[int, Row]:
with path.open(newline="", encoding="utf-8") as handle:
reader = csv.DictReader(handle)
rows: dict[int, Row] = {}
for raw in reader:
address = parse_hex(raw["address"])
rows[address] = Row(
address=address,
size=raw["size"],
name=raw["name"],
subsystem=raw["subsystem"],
calling_convention=raw["calling_convention"],
prototype_status=raw["prototype_status"],
source_tool=raw["source_tool"],
confidence=raw["confidence"],
notes=raw["notes"],
verified_against=raw["verified_against"],
)
return rows
def extract_note_refs(rows: dict[int, Row]) -> dict[int, set[int]]:
refs: dict[int, set[int]] = {}
known = set(rows)
for address, row in rows.items():
hits = {parse_hex(match.group(0)) for match in ADDRESS_RE.finditer(row.notes)}
refs[address] = {hit for hit in hits if hit in known and hit != address}
return refs
def build_backrefs(refs: dict[int, set[int]]) -> dict[int, set[int]]:
backrefs: dict[int, set[int]] = {address: set() for address in refs}
for src, dsts in refs.items():
for dst in dsts:
backrefs.setdefault(dst, set()).add(src)
return backrefs
def walk_subgraph(
rows: dict[int, Row],
refs: dict[int, set[int]],
seeds: list[int],
depth: int,
include_backrefs: bool,
) -> set[int]:
backrefs = build_backrefs(refs)
seen: set[int] = set()
queue: deque[tuple[int, int]] = deque((seed, 0) for seed in seeds if seed in rows)
while queue:
address, level = queue.popleft()
if address in seen:
continue
seen.add(address)
if level >= depth:
continue
for dst in sorted(refs.get(address, ())):
if dst not in seen:
queue.append((dst, level + 1))
if include_backrefs:
for src in sorted(backrefs.get(address, ())):
if src not in seen:
queue.append((src, level + 1))
return seen
def quote_dot(text: str) -> str:
return text.replace("\\", "\\\\").replace('"', '\\"')
def emit_dot(
rows: dict[int, Row],
refs: dict[int, set[int]],
included: set[int],
seeds: set[int],
output_path: Path,
title: str,
) -> None:
subsystems: dict[str, list[Row]] = {}
for address in sorted(included):
row = rows[address]
subsystems.setdefault(row.subsystem, []).append(row)
lines: list[str] = [
"digraph shell_load {",
' graph [rankdir=LR, labelloc="t", labeljust="l"];',
f' label="{quote_dot(title)}";',
' node [shape=box, style="rounded,filled", fillcolor="#f8f8f8", color="#555555", fontname="Helvetica"];',
' edge [color="#666666", fontname="Helvetica"];',
]
for subsystem in sorted(subsystems):
cluster_id = subsystem.replace("-", "_")
lines.append(f' subgraph cluster_{cluster_id} {{')
lines.append(f' label="{quote_dot(subsystem)}";')
lines.append(' color="#cccccc";')
for row in subsystems[subsystem]:
seed_mark = " [seed]" if row.address in seeds else ""
label = f"{fmt_addr(row.address)}\\n{row.name}{seed_mark}"
fill = "#ffe9a8" if row.address in seeds else "#f8f8f8"
lines.append(
f' "{fmt_addr(row.address)}" [label="{quote_dot(label)}", fillcolor="{fill}"];'
)
lines.append(" }")
for src in sorted(included):
for dst in sorted(refs.get(src, ())):
if dst not in included:
continue
lines.append(f' "{fmt_addr(src)}" -> "{fmt_addr(dst)}";')
lines.append("}")
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def emit_markdown(
rows: dict[int, Row],
refs: dict[int, set[int]],
included: set[int],
seeds: set[int],
output_path: Path,
title: str,
dot_path: Path,
) -> None:
included_rows = [rows[address] for address in sorted(included)]
edge_count = sum(
1
for src in included
for dst in refs.get(src, ())
if dst in included
)
lines = [
f"# {title}",
"",
f"- Nodes: `{len(included_rows)}`",
f"- Edges: `{edge_count}`",
f"- Seeds: {', '.join(f'`{fmt_addr(seed)}`' for seed in sorted(seeds))}",
f"- Graphviz: `{dot_path.name}`",
"",
"## Nodes",
"",
"| Address | Name | Subsystem | Confidence |",
"| --- | --- | --- | --- |",
]
for row in included_rows:
lines.append(
f"| `{fmt_addr(row.address)}` | `{row.name}` | `{row.subsystem}` | `{row.confidence}` |"
)
lines.extend(["", "## Edges", ""])
for src in sorted(included):
dsts = [dst for dst in sorted(refs.get(src, ())) if dst in included]
if not dsts:
continue
src_row = rows[src]
lines.append(f"- `{fmt_addr(src)}` `{src_row.name}`")
for dst in dsts:
dst_row = rows[dst]
lines.append(f" -> `{fmt_addr(dst)}` `{dst_row.name}`")
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def main() -> int:
args = parse_args()
rows = load_rows(args.function_map)
refs = extract_note_refs(rows)
seeds = [parse_hex(seed) for seed in args.seed]
included = walk_subgraph(rows, refs, seeds, args.depth, args.include_backrefs)
output_prefix = args.output_prefix.resolve()
output_prefix.parent.mkdir(parents=True, exist_ok=True)
dot_path = output_prefix.with_suffix(".dot")
md_path = output_prefix.with_suffix(".md")
emit_dot(rows, refs, included, set(seeds), dot_path, args.title)
emit_markdown(rows, refs, included, set(seeds), md_path, args.title, dot_path)
return 0
if __name__ == "__main__":
raise SystemExit(main())

43
tools/run_hook_auto_load.sh Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
log_path="$game_dir/rrt_hook_attach.log"
proxy_path="$game_dir/dinput8.dll"
save_stem="${1:-hh}"
timeout_seconds="${RRT_HOOK_TIMEOUT_SECONDS:-60}"
. "$HOME/.local/share/cargo/env"
cargo build -p rrt-hook --target i686-pc-windows-gnu
rm -f "$log_path"
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
echo "launcher: start=$(date --iso-8601=seconds)"
echo "launcher: save_stem=$save_stem timeout_seconds=$timeout_seconds"
set +e
(
cd "$game_dir"
export RRT_AUTO_LOAD_SAVE="$save_stem"
export WINEPREFIX="$repo_root/rt3_wineprefix"
export WINEDLLOVERRIDES="dinput8=n,b"
timeout "${timeout_seconds}s" /opt/wine-stable/bin/wine RT3.exe
) >/tmp/rrt-hook-auto-load-wine.log 2>&1 &
launcher_pid=$!
echo "launcher: wrapper_pid=$launcher_pid"
wait "$launcher_pid"
launcher_status=$?
set -e
echo "launcher: exit_status=$launcher_status"
echo "launcher: end=$(date --iso-8601=seconds)"
if [[ -f "$log_path" ]]; then
cat "$log_path"
else
echo "attach-log-missing" >&2
exit 1
fi

View file

@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
proxy_path="$game_dir/dinput8.dll"
save_stem="${1:-hh}"
. "$HOME/.local/share/cargo/env"
cargo build -p rrt-hook --target i686-pc-windows-gnu
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
cd "$game_dir"
export RRT_AUTO_LOAD_SAVE="$save_stem"
export WINEPREFIX="$repo_root/rt3_wineprefix"
export WINEDLLOVERRIDES="dinput8=n,b"
cmd=(/opt/wine-stable/bin/winedbg)
cmd_file="${RRT_WINEDBG_CMD_FILE:-$repo_root/tools/winedbg_auto_load_compare.cmd}"
if [[ -n "$cmd_file" ]]; then
cmd+=(--file "$cmd_file")
fi
cmd+=(RT3.exe)
log_file="${RRT_WINEDBG_LOG:-$repo_root/rt3_auto_load_winedbg.log}"
if [[ -n "$log_file" ]]; then
cmd_string="$(printf '%q ' "${cmd[@]}")"
exec script -qefc "${cmd_string% }" "$log_file"
fi
exec "${cmd[@]}"

View file

@ -13,9 +13,11 @@ cargo build -p rrt-hook --target i686-pc-windows-gnu
rm -f "$log_path"
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
#RRT_WRITE_FINANCE_TEMPLATE=1 \
(
cd "$game_dir"
timeout 8s env \
timeout 600s env \
RRT_WRITE_FINANCE_CAPTURE=1 \
WINEPREFIX="$repo_root/rt3_wineprefix" \
WINEDLLOVERRIDES="dinput8=n,b" \
/opt/wine-stable/bin/wine RT3.exe

29
tools/run_rt3_winedbg.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
game_dir="$repo_root/rt3_wineprefix/drive_c/rt3"
proxy_path="$game_dir/dinput8.dll"
. "$HOME/.local/share/cargo/env"
cargo build -p rrt-hook --target i686-pc-windows-gnu
cp "$repo_root/target/i686-pc-windows-gnu/debug/dinput8.dll" "$proxy_path"
cd "$game_dir"
export WINEPREFIX="$repo_root/rt3_wineprefix"
export WINEDLLOVERRIDES="dinput8=n,b"
cmd=(/opt/wine-stable/bin/winedbg)
cmd_file="${RRT_WINEDBG_CMD_FILE:-$repo_root/tools/winedbg_manual_load_445ac0.cmd}"
if [[ -n "$cmd_file" ]]; then
cmd+=(--file "$cmd_file")
fi
cmd+=(RT3.exe)
log_file="${RRT_WINEDBG_LOG:-$repo_root/rt3_manual_load_winedbg.log}"
if [[ -n "$log_file" ]]; then
cmd_string="$(printf '%q ' "${cmd[@]}")"
exec script -qefc "${cmd_string% }" "$log_file"
fi
exec "${cmd[@]}"

View file

@ -0,0 +1,76 @@
break *0x00438890
break *0x004390cb
break *0x00445ac0
break *0x0053fea6
cont
info reg
print/x *(unsigned int*)($esp)
print/x *(unsigned int*)($esp+4)
print/x *(unsigned int*)($esp+8)
print/x *(unsigned int*)($esp+12)
print/x *(unsigned int*)0x006cec74
print/x *(unsigned int*)0x006cec7c
print/x *(unsigned int*)0x006cec78
print/x *(unsigned int*)0x006ce9b8
print/x *(unsigned int*)0x006ce9bc
print/x *(unsigned int*)0x006ce9c0
print/x *(unsigned int*)0x006ce9c4
print/x *(unsigned int*)0x006d1270
print/x *(unsigned int*)0x006d1274
print/x *(unsigned int*)0x006d1278
print/x *(unsigned int*)0x006d127c
bt
cont
info reg
print/x *(unsigned int*)($esp)
print/x *(unsigned int*)($esp+4)
print/x *(unsigned int*)($esp+8)
print/x *(unsigned int*)($esp+12)
print/x *(unsigned int*)0x006cec74
print/x *(unsigned int*)0x006cec7c
print/x *(unsigned int*)0x006cec78
print/x *(unsigned int*)0x006ce9b8
print/x *(unsigned int*)0x006ce9bc
print/x *(unsigned int*)0x006ce9c0
print/x *(unsigned int*)0x006ce9c4
print/x *(unsigned int*)0x006d1270
print/x *(unsigned int*)0x006d1274
print/x *(unsigned int*)0x006d1278
print/x *(unsigned int*)0x006d127c
bt
cont
info reg
print/x *(unsigned int*)($esp)
print/x *(unsigned int*)($esp+4)
print/x *(unsigned int*)($esp+8)
print/x *(unsigned int*)($esp+12)
print/x *(unsigned int*)0x006cec74
print/x *(unsigned int*)0x006cec7c
print/x *(unsigned int*)0x006cec78
print/x *(unsigned int*)0x006ce9b8
print/x *(unsigned int*)0x006ce9bc
print/x *(unsigned int*)0x006ce9c0
print/x *(unsigned int*)0x006ce9c4
print/x *(unsigned int*)0x006d1270
print/x *(unsigned int*)0x006d1274
print/x *(unsigned int*)0x006d1278
print/x *(unsigned int*)0x006d127c
bt
cont
info reg
print/x *(unsigned int*)($esp)
print/x *(unsigned int*)($esp+4)
print/x *(unsigned int*)($esp+8)
print/x *(unsigned int*)($esp+12)
print/x *(unsigned int*)0x006cec74
print/x *(unsigned int*)0x006cec7c
print/x *(unsigned int*)0x006cec78
print/x *(unsigned int*)0x006ce9b8
print/x *(unsigned int*)0x006ce9bc
print/x *(unsigned int*)0x006ce9c0
print/x *(unsigned int*)0x006ce9c4
print/x *(unsigned int*)0x006d1270
print/x *(unsigned int*)0x006d1274
print/x *(unsigned int*)0x006d1278
print/x *(unsigned int*)0x006d127c
bt

View file

@ -0,0 +1,20 @@
break *0x004390cb
break *0x00445ac0
cont
info reg
print/x *(unsigned int*)($esp)
print/x *(unsigned int*)($esp+4)
print/x *(unsigned int*)($esp+8)
print/x *(unsigned int*)($esp+12)
print/x *(unsigned int*)0x006cec74
print/x *(unsigned int*)0x006cec7c
print/x *(unsigned int*)0x006cec78
print/x *(unsigned int*)0x006ce9b8
print/x *(unsigned int*)0x006ce9bc
print/x *(unsigned int*)0x006ce9c0
print/x *(unsigned int*)0x006ce9c4
print/x *(unsigned int*)0x006d1270
print/x *(unsigned int*)0x006d1274
print/x *(unsigned int*)0x006d1278
print/x *(unsigned int*)0x006d127c
bt