#!/usr/bin/env python3 """Lane V2 -- write the resolved indirect edges back into Ghidra. Everything here comes from tools/vtable_map.py (RTTI vftable map + slot recovery + constructor-derived member typing). Nothing is decompiler-derived. uv run python3 scripts/lane_v2_writeback.py [--dry] """ import json import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.dirname(HERE) PROG = "/Sword of the Stars.exe" DRY = "--dry" in sys.argv def call(tool, args): if DRY: print(f" [dry] {tool} {json.dumps(args)[:150]}") return p = subprocess.run( ["uv", "run", "python3", os.path.join(REPO, "tools", "reva_call.py"), tool, json.dumps(args)], cwd=REPO, capture_output=True, text=True, timeout=300) ok = '"success":true' in p.stdout or '"success": true' in p.stdout print(f" {'ok ' if ok else 'ERR'} {tool} {args.get('addressOrSymbol', args.get('address', ''))}" f" {'' if ok else p.stdout.strip()[:200] + p.stderr.strip()[:200]}") def comment(addr, text): call("set-comment", {"programPath": PROG, "addressOrSymbol": addr, "comment": text, "commentType": "pre"}) def plate(addr, text): call("set-comment", {"programPath": PROG, "addressOrSymbol": addr, "comment": text, "commentType": "plate"}) def label(addr, name): call("create-label", {"programPath": PROG, "addressOrSymbol": addr, "labelName": name}) TRADE_VT = "0x00a31b74" SPY_VT = "0x00a3073c" # --------------------------------------------------------------- the targets # vtable, class, slot, target, phase note, RNG note TARGETS = [ (TRADE_VT, "ServerTradeManagerImpl", 10, "0x00893290", "dispatched at 0x007d8469 (StrategyServer::DetectEncounters)", "DRAWS: Chance x3 at 0x00893426 / 0x00893513 / 0x008935ce on the " "strategic generator; 16 of ~20 words of a strategic turn (lane Z)"), (TRADE_VT, "ServerTradeManagerImpl", 14, "0x008590d0", "OnAllCombatDone_Tail phase 23, call 1 of 8, at 0x007d97a7", ""), (TRADE_VT, "ServerTradeManagerImpl", 8, "0x0088e8d0", "OnAllCombatDone_Tail phase 23, call 2 of 8, at 0x007d97b6 (arg 1)", ""), (TRADE_VT, "ServerTradeManagerImpl", 12, "0x0088e920", "OnAllCombatDone_Tail phase 23, call 3 of 8, at 0x007d97c3", ""), (TRADE_VT, "ServerTradeManagerImpl", 11, "0x00848570", "OnAllCombatDone_Tail phase 23, call 4 of 8, at 0x007d97d0", ""), (TRADE_VT, "ServerTradeManagerImpl", 9, "0x00868060", "OnAllCombatDone_Tail phase 23, call 5 of 8, at 0x007d97dd", ""), (TRADE_VT, "ServerTradeManagerImpl", 7, "0x0088ad60", "OnAllCombatDone_Tail phase 23, call 6 of 8, at 0x007d97ea", ""), (TRADE_VT, "ServerTradeManagerImpl", 13, "0x0088ef80", "OnAllCombatDone_Tail phase 23, call 7 of 8, at 0x007d97f7", "REACHES A DRAW: -> 0x00820ca0 NextFloat at 0x00820e18 and " "-> 0x0088b440 NextInt at 0x0088b613, both on [obj+0x16c] = the " "StrategyServer strategic generator. Never observed firing (lane Z " "measured 0 tail words on 8 turns)"), (TRADE_VT, "ServerTradeManagerImpl", 15, "0x0082cca0", "OnAllCombatDone_Tail phase 23, call 8 of 8, at 0x007d9804", "REACHES A DRAW: Chance at 0x0082cdb8, generator loaded at 0x0082cda4 " "as [eax+0x16c] = the strategic generator. Never observed firing"), (SPY_VT, "ServerSpyManager", 13, "0x008877b0", "OnAllCombatDone_Tail phase 23, the ninth call, at 0x007d9811 " "(receiver is StrategyServer+0x15c, not +0x158)", "REACHES A DRAW: Chance at 0x00887c8a on [ecx+0x16c]; and via " "0x008408e0 Chance at 0x00840929 / 0x00840a3c and NextInt at " "0x008409c7, all on [reg+0x16c] = the strategic generator. " "Never observed firing"), (SPY_VT, "ServerSpyManager", 14, "0x0088db80", "OnAllCombatDone_Tail phase 33, call 1 of 2, at 0x007d989b", "REACHES A DRAW: Chance at 0x0088dc43 on [eax+0x16c] = the strategic " "generator. Never observed firing"), (SPY_VT, "ServerSpyManager", 15, "0x00887f30", "OnAllCombatDone_Tail phase 33, call 2 of 2, at 0x007d98a8", ""), ] # dispatch site -> (class, slot, target) SITES = [ ("0x007d8469", "ServerTradeManagerImpl", 10, "0x00893290", TRADE_VT), ("0x007d97a7", "ServerTradeManagerImpl", 14, "0x008590d0", TRADE_VT), ("0x007d97b6", "ServerTradeManagerImpl", 8, "0x0088e8d0", TRADE_VT), ("0x007d97c3", "ServerTradeManagerImpl", 12, "0x0088e920", TRADE_VT), ("0x007d97d0", "ServerTradeManagerImpl", 11, "0x00848570", TRADE_VT), ("0x007d97dd", "ServerTradeManagerImpl", 9, "0x00868060", TRADE_VT), ("0x007d97ea", "ServerTradeManagerImpl", 7, "0x0088ad60", TRADE_VT), ("0x007d97f7", "ServerTradeManagerImpl", 13, "0x0088ef80", TRADE_VT), ("0x007d9804", "ServerTradeManagerImpl", 15, "0x0082cca0", TRADE_VT), ("0x007d9811", "ServerSpyManager", 13, "0x008877b0", SPY_VT), ("0x007d989b", "ServerSpyManager", 14, "0x0088db80", SPY_VT), ("0x007d98a8", "ServerSpyManager", 15, "0x00887f30", SPY_VT), ] # the SVScriptObject hook block on StrategyServer+0x1b4 SVSO = [ ("0x007dcb8e", 4, "ProcessTurn, hook id 6 pushed at 0x007dcb8c"), ("0x007dcb97", 11, "ProcessTurn, the hook body paired with the id-6 gate"), ("0x007dcbb6", 4, "ProcessTurn, second gate"), ("0x007dcbbf", 30, "ProcessTurn, the hook body paired with it"), ("0x007d9767", 4, "OnAllCombatDone_Tail, gate"), ("0x007d9770", 25, "OnAllCombatDone_Tail, hook body"), ("0x007d9783", 4, "OnAllCombatDone_Tail, gate"), ("0x007d978c", 27, "OnAllCombatDone_Tail, hook body"), ("0x007d9838", 4, "OnAllCombatDone_Tail, gate"), ("0x007d9841", 30, "OnAllCombatDone_Tail, hook body"), ] SVSO_NOTE = ( "SVScriptObject hook: receiver is StrategyServer+0x1b4, null-checked " "before the block (`cmp reg,ebx; je`). 30 classes derive from " "Game::SVScriptObject; the base's empty override is 0x0080c5a0. " "Lane V2: slot 4 has 14 non-stub overrides, slot 11 has 7, slot 25 has 8, " "slot 30 has 1, slot 27 has 0. Thirteen of those overrides reach an RNG " "draw at depth 2-4 (e.g. SVSOCrowRuins slot 11 = 0x00518340 -> " "0x004f4210 -> IntRangeBell 0x008e6d80). Which generator object those " "draws use is NOT established -- it arrives as an argument. " "Scripted-scenario only; presumed inactive in a normal game and still " "not proven so." ) def main(): print("== vtable-slot plate comments and labels ==") for vt, cls, slot, tgt, where, rng in TARGETS: txt = (f"Game::{cls} vftable {vt} slot {slot} (+0x{4 * slot:x}).\n" f"Reached ONLY through a virtual dispatch: {where}.\n" f"Receiver typed from the constructor: StrategyServer ctor " f"0x007d78d0 stores the {cls} constructor's result into " f"StrategyServer+0x{'158' if cls.startswith('ServerTrade') else '15c'}" f"; corroborated by 0x007dcf90 (a StrategyServer method entered " f"on the +4 sub-object) storing the same two constructors 4 " f"bytes lower. [lane V2]") if rng: txt += "\n" + rng plate(tgt, txt) label(tgt, f"{cls}_vslot{slot}") print("== dispatch-site comments ==") for site, cls, slot, tgt, vt in SITES: comment(site, f"-> Game::{cls}::vslot{slot} = {tgt} " f"(vftable {vt} + 0x{4 * slot:x}) [lane V2]") print("== SVScriptObject hook block ==") for site, slot, where in SVSO: comment(site, f"SVScriptObject slot {slot} (+0x{4 * slot:x}) -- {where}." f"\n{SVSO_NOTE} [lane V2]") print("== the two managers ==") plate("0x007d78d0", "Game::StrategyServer constructor (vptr installs: 0x00a26084 at +0, " "0x00a26034 at +4, Mars::IStreamable 0x009e22bc at +0).\n" "Lane V2 member typings taken from here:\n" " +0x158 = Game::ServerTradeManagerImpl* (ctor 0x00858f70, " "stored 0x007d7d81)\n" " +0x15c = Game::ServerSpyManager* (ctor 0x00832a30, " "stored 0x007d7d8e)\n" "Game::ServerTradeManager (vftable 0x00a311a4) is abstract -- 21 of " "its 22 slots are purecall -- and ServerTradeManagerImpl is its " "ONLY derived class, so slot N of that interface resolves uniquely. " "Game::ServerSpyManager has no *Impl: it is itself the concrete " "class (18 slots, none purecall) over IServerSpyManager / " "ISpyManager / IStreamable. [lane V2]") return 0 if __name__ == "__main__": sys.exit(main())