sots-re/ghidra/scripts/LoaderWriteBack.java

258 lines
37 KiB
Java

import ghidra.app.script.GhidraScript;
import ghidra.app.cmd.function.ApplyFunctionSignatureCmd;
import ghidra.app.util.parser.FunctionSignatureParser;
import ghidra.program.model.address.*;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.program.model.data.*;
import ghidra.app.decompiler.*;
import java.util.*;
import java.io.*;
// Write-back for the data-loader round (2026-09-07): GlobalConst config plumbing, Mars::Script brace parser,
// catalog loaders (weapons/sections/tech), gobio VFS, TextFileStream (.effect), and the Application spine
// signatures (Initialize/Run/OnTick). Log: /tmp/ldr/writeback.log. Verification decompiles: /tmp/ldr/verify/.
public class LoaderWriteBack extends GhidraScript {
DataTypeManager dtm; SymbolTable st; PrintWriter log; DecompInterface decomp;
CategoryPath CAT = new CategoryPath("/SOTS");
static final String DOC = "Source: /srv/re-lab/handoff/loader-prototypes.md (data-loader prototypes, 2026-09-07)";
Map<String, DataType> types = new HashMap<String, DataType>();
List<Long> verify = new ArrayList<Long>();
DataType prim(String k) {
if (k.equals("int")) return IntegerDataType.dataType;
if (k.equals("uint")) return UnsignedIntegerDataType.dataType;
if (k.equals("float")) return FloatDataType.dataType;
if (k.equals("bool")) return BooleanDataType.dataType;
if (k.equals("u8")) return ByteDataType.dataType;
if (k.equals("ptr")) return new PointerDataType(VoidDataType.dataType);
if (k.equals("cstr")) return new PointerDataType(CharDataType.dataType);
if (k.endsWith("*")) { DataType t = types.get(k.substring(0, k.length()-1)); return new PointerDataType(t == null ? VoidDataType.dataType : t); }
if (k.startsWith("char[")) { int n = Integer.parseInt(k.substring(5, k.length()-1)); return new ArrayDataType(CharDataType.dataType, n, 1); }
if (k.startsWith("float[")) { int n = Integer.parseInt(k.substring(6, k.length()-1)); return new ArrayDataType(FloatDataType.dataType, n, 4); }
DataType t = types.get(k); if (t != null) return t;
throw new RuntimeException("unknown type " + k);
}
Structure mk(String name, int size, String desc) throws Exception {
StructureDataType s = new StructureDataType(CAT, name, size, dtm);
s.setDescription(desc + "\n" + DOC);
Structure r = (Structure) dtm.addDataType(s, DataTypeConflictHandler.REPLACE_HANDLER);
types.put(name, r); log.println(" type " + r.getPathName() + " size=" + r.getLength()); return r;
}
void fields(Structure s, Object[][] rows) {
for (Object[] r : rows) { int off = ((Number) r[0]).intValue(); DataType dt = prim((String) r[1]); String name = (String) r[2]; String cmt = r.length > 3 ? (String) r[3] : null;
try { s.replaceAtOffset(off, dt, dt.getLength(), name, cmt); } catch (Exception e) { log.println(" !! " + s.getName() + "." + name + ": " + e.getMessage()); } }
}
Namespace ns(String path) throws Exception {
Namespace cur = currentProgram.getGlobalNamespace();
for (String part : path.split("::")) { Namespace n = st.getNamespace(part, cur); if (n == null) n = st.createClass(cur, part, SourceType.USER_DEFINED); cur = n; }
return cur;
}
// rename + prototype. sig: C declaration using the function's new simple name, e.g. "bool Open(Script * this, char * path)"
void fn(long addr, String nsPath, String name, String conv, String sig, String comment) {
Address a = toAddr(addr); Function f = getFunctionAt(a);
try {
if (f == null) { f = createFunction(a, name); if (f == null) { log.println("!! no function at " + a); return; } }
try { f.getSymbol().setNameAndNamespace(name, ns(nsPath), SourceType.USER_DEFINED); }
catch (ghidra.util.exception.DuplicateNameException d) { for (Symbol s2 : st.getSymbols(a)) { if (s2.getName().equals(name) && !s2.isPrimary()) { s2.delete(); } } f.getSymbol().setNameAndNamespace(name, ns(nsPath), SourceType.USER_DEFINED); }
if (sig != null) {
FunctionDefinitionDataType fd = buildDef(name, sig);
if (conv != null) fd.setCallingConvention(conv);
ApplyFunctionSignatureCmd cmd = new ApplyFunctionSignatureCmd(a, fd, SourceType.USER_DEFINED);
if (!cmd.applyTo(currentProgram)) log.println("!! sig failed " + a + ": " + cmd.getStatusMsg());
}
if (conv != null) f.setCallingConvention(conv);
if (comment != null) { String old = f.getComment(); f.setComment((old == null || old.isEmpty() ? "" : old + "\n") + comment + "\n" + DOC); }
log.println(" fn " + a + " " + f.getName(true) + " :: " + f.getSignature().getPrototypeString() + " [" + f.getCallingConventionName() + "]");
} catch (Exception e) { log.println("!! fn " + a + " " + name + ": " + e); }
verify.add(addr);
}
DataType resolve(String t) {
t = t.trim(); int stars = 0; while (t.endsWith("*")) { stars++; t = t.substring(0, t.length()-1).trim(); }
DataType b;
if (t.equals("void")) b = VoidDataType.dataType; else if (t.equals("char")) b = CharDataType.dataType; else if (t.equals("byte")) b = ByteDataType.dataType; else b = prim(t);
for (int i = 0; i < stars; i++) b = new PointerDataType(b);
return b;
}
FunctionDefinitionDataType buildDef(String name, String sig) {
int lp = sig.indexOf('('), rp = sig.lastIndexOf(')');
String head = sig.substring(0, lp).trim(); String ret = head.substring(0, head.lastIndexOf(' ')).trim();
String args = sig.substring(lp+1, rp).trim();
FunctionDefinitionDataType fd = new FunctionDefinitionDataType(name);
fd.setReturnType(resolve(ret));
List<ParameterDefinition> ps = new ArrayList<ParameterDefinition>();
if (!args.isEmpty() && !args.equals("void")) for (String p : args.split(",")) { p = p.trim(); int sp = p.lastIndexOf(' '); String pn = p.substring(sp+1).trim(); String pt = p.substring(0, sp).trim(); ps.add(new ParameterDefinitionImpl(pn, resolve(pt), null)); }
fd.setArguments(ps.toArray(new ParameterDefinition[0]));
return fd;
}
void glob(long addr, String name, String type, String comment) {
try { Address a = toAddr(addr); createLabel(a, name, true, SourceType.USER_DEFINED);
DataType dt = prim(type); try { currentProgram.getListing().clearCodeUnits(a, a.add(dt.getLength()-1), false); currentProgram.getListing().createData(a, dt); } catch (Exception e) { log.println(" (data) " + name + ": " + e.getMessage()); }
if (comment != null) setEOLComment(a, comment); log.println(" global " + a + " " + name); }
catch (Exception e) { log.println("!! global " + name + ": " + e); }
}
void verifyDump() throws Exception {
new File("/tmp/ldr/verify").mkdirs();
for (long addr : verify) { Function f = getFunctionAt(toAddr(addr)); if (f == null) continue;
PrintWriter w = new PrintWriter(new FileWriter(String.format("/tmp/ldr/verify/%08x.c", addr)));
w.println("// " + f.getName(true) + " :: " + f.getSignature().getPrototypeString() + " [" + f.getCallingConventionName() + "]");
try { DecompileResults r = decomp.decompileFunction(f, 600, monitor); w.println(r.decompileCompleted() ? r.getDecompiledFunction().getC() : "[failed] " + r.getErrorMessage()); } catch (Exception e) { w.println("[exc] " + e); }
w.close(); }
}
@Override public void run() throws Exception {
dtm = currentProgram.getDataTypeManager(); st = currentProgram.getSymbolTable();
new File("/tmp/ldr").mkdirs(); log = new PrintWriter(new FileWriter("/tmp/ldr/writeback.log"));
decomp = new DecompInterface(); decomp.openProgram(currentProgram);
// ---------- types ----------
log.println("== types");
String[] opaque = {"Application","DemoApp","AppStartup","IConfigCallback","WeaponDictionary","WeaponDef","SectionDictionary","SectionDef","MasterTechTree","TechTree","Stream","IStreamable","ParticleSystem","EffectDictionary","StringVector","IFileSystem","GlobalConstMap","NativeFileSystem","ZipFileSystem"};
for (String o : opaque) { if (dtm.getDataType(CAT, o) == null && types.get(o) == null) { StructureDataType s = new StructureDataType(CAT, o, 0, dtm); s.setDescription("opaque (referenced by pointer)\n" + DOC); types.put(o, dtm.addDataType(s, DataTypeConflictHandler.KEEP_HANDLER)); } else { types.put(o, dtm.getDataType(CAT, o)); } }
// GlobalConst parse fn pointer
FunctionDefinitionDataType pf = new FunctionDefinitionDataType(CAT, "GlobalConstParseFn", dtm);
pf.setReturnType(VoidDataType.dataType); pf.setArguments(new ParameterDefinition[]{ new ParameterDefinitionImpl("storage", new PointerDataType(VoidDataType.dataType), "int*/float*/float[4]"), new ParameterDefinitionImpl("text", new PointerDataType(CharDataType.dataType), "value token") });
try { pf.setCallingConvention("__cdecl"); } catch (Exception e) {}
DataType pfd = dtm.addDataType(pf, DataTypeConflictHandler.REPLACE_HANDLER); types.put("GlobalConstParseFn", new PointerDataType(pfd));
dtm.addDataType(new TypedefDataType(CAT, "GlobalConstParseFnPtr", new PointerDataType(pfd), dtm), DataTypeConflictHandler.REPLACE_HANDLER);
Structure gc = mk("GlobalConst", 0x10, "One registered data-file constant (static-init stub: push file; push parser; push key; push &storage; mov ecx,&cfgvar; call GlobalConst::GlobalConst). Code reads *storage.");
fields(gc, new Object[][]{ {0,"ptr","storage","int* / float* / float[4] the code reads"}, {4,"cstr","key","KEY token, matched case-insensitively (_stricmp)"}, {8,"GlobalConstParseFn","parser","0x008b7000 int, 0x008b7020 float, 0x008b70e0 float*k, 0x008b7110 colour"}, {0xc,"cstr","file","data file this key lives in, e.g. Data/Strategy/StrategyVars.txt"} });
Structure ib = mk("IBuffer", 0x10, "Mars::gobio::Buffer: refcounted whole-file buffer returned by gobio::ReadFile. vtable: [0] dtor(flags) [1] Data() [2] Data() [3] Size()");
fields(ib, new Object[][]{ {0,"ptr","vftable"}, {4,"int","refcount","RefCounted_Release (0x008a7230) decrements; deletes at 0"}, {8,"ptr","data"}, {0xc,"uint","size"} });
Structure sc = mk("Script", 0x28, "Mars::Script: whitespace tokenizer over a gobio buffer (brace-block files, manifests, KEY value tables)");
fields(sc, new Object[][]{ {0,"ptr","vftable","Mars::Script::vftable"}, {4,"char[16]","path_buf","std::string path (SSO buffer / pointer)"}, {0x14,"uint","path_size"}, {0x18,"uint","path_cap"}, {0x20,"IBuffer*","buf","whole file (NULL when not open)"}, {0x24,"uint","cursor","byte offset into buf->data"} });
Structure tk = mk("ScriptToken", 0x804, "Token record filled by Mars::Script::Next: type 0 = none/EOF, 1 = key value, 2 = key { (block open), 3 = } (block close)");
fields(tk, new Object[][]{ {0,"int","type"}, {4,"char[1024]","key","first token (key / block name / '}')"}, {0x404,"char[1024]","value","second token (value, or '{')"} });
Structure fs = mk("FileSystemSet", 0x14, "Mars::gobio file-system set (singleton at 0x00b2e274, pointer at 0x00b2e270). Open() tries file systems in vector order: native '.' first, then sots.ini [Modules] Mount<N> in N order (default table: sots.gob, sots_local_en.gob)");
fields(fs, new Object[][]{ {0,"ptr","fs_begin","std::vector<IFileSystem*> begin"}, {4,"ptr","fs_end"}, {8,"ptr","fs_cap"}, {0xc,"int","pad"}, {0x10,"int","current","index of the file system holding the open file, -1 none"} });
Structure nfs = mk("NativeFileSystemImpl", 0x28, "Mars::gobio::NativeFileSystem (loose files under root, fopen/fread)");
fields(nfs, new Object[][]{ {0,"ptr","vftable"}, {4,"bool","valid"}, {8,"char[16]","root_buf","std::string root (empty = cwd), trailing slash appended"}, {0x18,"uint","root_size"}, {0x1c,"uint","root_cap"}, {0x24,"ptr","file","FILE* of the open file"} });
Structure zfs = mk("ZipFileSystemImpl", 0x34, "Mars::gobio::ZipFileSystem (.gob = zip; entries matched case-insensitively with \\ and / equal)");
fields(zfs, new Object[][]{ {0,"ptr","vftable"}, {4,"char[16]","path_buf","std::string archive path"}, {0x14,"uint","path_size"}, {0x18,"uint","path_cap"}, {0x20,"ptr","unz","unzip handle"}, {0x28,"ptr","entries","std::map<string,entry> header (case-insensitive path compare)"} });
Structure tfs = mk("TextFileStream", 0x420, "Mars::TextFileStream: 'TXT' magic line + 'LABEL value' lines + LABEL/BEGIN/END nesting (.effect files). Base FileStream 0x1c bytes.");
fields(tfs, new Object[][]{ {0,"ptr","vftable","[1] ReadString(label,buf,max) [2] ReadBool [3] ReadFloat [4] ReadInt [5] ReadNested(label,IStreamable*|NULL=skip) [6..10] Write*"}, {4,"bool","label_missing_reported"}, {8,"int","mode","1 read (gobio buffer), 2 write (FILE*)"}, {0xc,"IBuffer*","buf"}, {0x10,"uint","cursor"}, {0x14,"ptr","file","FILE* (write mode)"}, {0x18,"int","indent"}, {0x1c,"char[1024]","line","current line"}, {0x41c,"bool","line_pushed_back","true when the last line did not match the requested label"} });
Structure wd = mk("WeaponDefPartial", 0x278, "Game::WeaponDef (0x278 bytes) — only the fields confirmed from WeaponDef::WeaponDef / ParseScript / dictionary lookups");
fields(wd, new Object[][]{ {0,"int","index","position in WeaponDictionary vector (rewritten after sort)"}, {4,"int","id","stable id from Weapons/_weapons.txt"}, {8,"char[16]","path_buf","std::string file path"}, {0x18,"uint","path_size"}, {0x1c,"uint","path_cap"}, {0x40,"char[16]","name_buf","std::string 'name' (looked up with _stricmp)"}, {0x50,"uint","name_size"}, {0x54,"uint","name_cap"}, {0x5c,"ptr","payload","behaviour sub-block object (bolt/beam/missile...), created by 0x00597ae0 from the block name"}, {0x70,"int","damagetype","7 = default"}, {0x84,"int","weaponclass","enum index into table 0x00a02760 (stride 0xc, +8 name)"}, {0x88,"int","weaponfamily"}, {0xb8,"int","turretclass","0x18 = unset"}, {0xbc,"int","turretsize","4 = unset"}, {0xc0,"int","munitionsize"}, {0xc4,"int","cpoints"}, {0xc8,"int","cost"}, {0xcc,"float","hpbonus"}, {0x264,"int","payload_kind","index of the behaviour block name in table 0x0058f230 (17 kinds)"} });
// ---------- M1: GlobalConst plumbing ----------
log.println("== M1");
fn(0x008b76a0L, "Mars::GlobalConst", "GlobalConst", "__thiscall", "GlobalConst * GlobalConst(GlobalConst * this, void * storage, char * key, GlobalConstParseFn parser, char * file)", "Static-init constructor for one data-file constant (~600 stubs at 0x009a4720..0x009c1400). Stores {storage,key,parser,file} then Register(). The file loader (GlobalConsts::LoadAll) later overwrites *storage from the data file, so the image value is only a fallback.");
fn(0x008b7610L, "Mars::GlobalConst", "Register", "__cdecl", "void Register(GlobalConst * g)", "Inserts g into g_GlobalConstRegistry (std::map<const char*,GlobalConst*> with _stricmp ordering, so keys are case-insensitive). Logs 'GlobalConsts: %s already registered' on duplicates and 'being registered after loading const table' if g_GlobalConstsLoaded.");
fn(0x008b7000L, "Mars::GlobalConst", "ParseInt", "__cdecl", "void ParseInt(int * storage, char * text)", "sscanf(text, \"%d\")");
fn(0x008b7020L, "Mars::GlobalConst", "ParseFloat", "__cdecl", "void ParseFloat(float * storage, char * text)", "sscanf(text, \"%f\")");
fn(0x008b70e0L, "Mars::GlobalConst", "ParseFloatScaled", "__cdecl", "void ParseFloatScaled(float * storage, char * text)", "sscanf %f then *storage *= *(float*)0x00af5210 (degrees->radians style scale)");
fn(0x008b7110L, "Mars::GlobalConst", "ParseColour", "__cdecl", "void ParseColour(float * rgba, char * text)", "sscanf(text, \"%d %d %d %d\") with defaults 255; each /255 and clamped to 0..1 into float[4] (missing alpha -> 1.0)");
fn(0x008b76d0L, "Mars::GlobalConsts", "LoadAll", "__cdecl", "void LoadAll(void)", "Called once from Application::Initialize. Collects the distinct file names of every registered GlobalConst, builds a per-file std::map<key,GlobalConst*> and calls LoadFile(file, map) for each; sets g_GlobalConstsLoaded.");
fn(0x008b73c0L, "Mars::GlobalConsts", "LoadFile", "__cdecl", "void LoadFile(char * file, GlobalConstMap * consts)", "Opens file with Mars::Script (gobio), loops Script::Next: type-1 pairs are looked up case-insensitively in consts; found -> parser(storage, valueToken) and the entry is erased (so a second occurrence logs '[file] KEY not recognized or is multiply defined' => first occurrence wins); blocks are skipped with SkipBlock(1); leftovers log '[file] KEY expected but not found'.");
glob(0x00b2d740L, "g_GlobalConstsLoaded", "bool", "set by GlobalConsts::LoadAll");
glob(0x00b2d744L, "g_GlobalConstRegistry", "ptr", "std::map<const char*,GlobalConst*,stricmp_less> object (+4 = header node ptr at 0x00b2d748)");
// ---------- M3: Mars::Script ----------
log.println("== M3 Script");
fn(0x008cd700L, "Mars::Script", "Script", "__thiscall", "Script * Script(Script * this)", "ctor: vftable, empty path, buf = NULL, cursor = 0");
fn(0x008cd720L, "Mars::Script", "~Script", "__thiscall", "void ~Script(Script * this)", null);
fn(0x008cd770L, "Mars::Script", "ScalarDeletingDtor", "__thiscall", "Script * ScalarDeletingDtor(Script * this, byte flags)", null);
fn(0x008cd7d0L, "Mars::Script", "Open", "__thiscall", "bool Open(Script * this, char * path)", "path -> std::string; releases the previous buffer; gobio::ReadFile(path, &buf). Returns false if no file system has the file.");
fn(0x008cd6c0L, "Mars::Script", "Close", "__thiscall", "void Close(Script * this)", "release buffer, clear path, cursor = 0");
fn(0x008cd2f0L, "Mars::Script", "ReadToken", "__thiscall", "int ReadToken(Script * this, char * out, uint outMax)", "Returns 0 = token read, 1 = no buffer / cursor past end, 2 = end of buffer reached (also when the token just read was the last byte of the file). Skips whitespace (space,\\t,\\r,\\n); a token starting with // is a comment to end of line; quotes \" ' ` delimit a token (no escapes, closing quote = same char); barewords end only at whitespace (so 'name{' is one token); output truncated silently at outMax-1.");
fn(0x008cd3e0L, "Mars::Script", "Next", "__thiscall", "int Next(Script * this, ScriptToken * tok)", "Reads one or two tokens: first token \"}\" -> tok->type = 3 (returns the ReadToken status of that token); otherwise reads a second token: \"{\" -> type 2 (block open, key = block name), else type 1 (key value). Returns the second ReadToken's status: callers loop while it is 0, so a final pair with no trailing whitespace/newline is dropped, and EOF inside a block simply ends parsing (lenient close).");
fn(0x008cd4b0L, "Mars::Script", "SkipBlock", "__thiscall", "int SkipBlock(Script * this, int depth)", "Consumes tokens counting { / } until depth reaches 0. Returns 0 normally, nonzero if EOF hit first.");
fn(0x008cd1f0L, "Mars::Script", "ScanToken", null, null, "register-convention helper (EAX=cursor ptr, ECX=&quotedFlag, EDX=out; stack: end, outEnd, &len): skips whitespace then quoted or bareword token");
fn(0x008cd180L, "Mars::Script", "ScanBareword", null, null, "copies bytes until space/\\t/\\r/\\n (braces are NOT delimiters)");
fn(0x008cd2b0L, "Mars::Script", "SkipLineComment", null, null, "advance to after the next \\n/\\r run");
fn(0x008cd820L, "Mars::ConfigParser", "ParseFile", "__cdecl", "void ParseFile(IConfigCallback * cb, char * path, char * blockName, bool * found)", "Opens path with Mars::Script; blockName NULL/empty -> cb->vft[1](script) on the whole file; else scans top-level blocks and calls cb->vft[1](script) for the block whose name matches blockName (_stricmp), SkipBlock(1) otherwise. Used for the startup config and GUI .script files.");
fn(0x008a7230L, "Mars", "RefCounted_Release", "__fastcall", "int RefCounted_Release(IBuffer * obj)", "--refcount; vft[0](1) (deleting dtor) when it reaches 0. Used for gobio buffers and other refcounted engine objects.");
// ---------- M2 / catalogs ----------
log.println("== M2 catalogs");
fn(0x0059a4c0L, "Game::WeaponDictionary", "Init", "__thiscall", "void Init(WeaponDictionary * this)", "Reads Weapons/_weapons.txt with Script::ReadToken pairs: id = atoi(tok1), file = tok2 (// DELETED lines are comments); LoadWeapon(\"Weapons/\"+file, id, NULL). Then sorts the vector (this+8..+0xc) and rewrites every def->index; this+0x18 = FindByName(\"emt_light\").");
fn(0x0059a230L, "Game::WeaponDictionary", "LoadWeapon", "__thiscall", "void LoadWeapon(WeaponDictionary * this, char * path, int id, WeaponDef * * out)", "Script::Open(path); loops Next(): the first block whose name is \"weapon\" (_stricmp) -> new WeaponDef(index, id, path); WeaponDef::ParseScript(def, script, &rc, this->techTree); success -> push_back into this+8 vector (and *out); failure -> 'WeaponDictionary::Init: failed to load: %s'. Other blocks: SkipBlock(1).");
fn(0x00599070L, "Game::WeaponDef", "ParseScript", "__thiscall", "bool ParseScript(WeaponDef * this, Script * script, int * rc, TechTree * tree)", "Loop while *rc == 0: *rc = Script::Next(script,&tok); type 1 -> if/else chain of _stricmp(tok.key, \"hidden\"|\"name\"|\"weaponclass\"|...) (66 keys, case-insensitive; unknown keys ignored); type 2 -> block name looked up in the 17-entry behaviour table (0x0058f230) -> payload object created (0x00597ae0) and its vft[1](script) parses the sub-block, otherwise SkipBlock(1); type 3 -> break. Warns when turretclass/turretsize were not given.");
fn(0x00598a20L, "Game::WeaponDef", "WeaponDef", "__thiscall", "WeaponDef * WeaponDef(WeaponDef * this, int index, int id, char * path)", null);
fn(0x00590a10L, "Game::WeaponDictionary", "FindByName", "__thiscall", "WeaponDef * FindByName(WeaponDictionary * this, char * name)", "linear scan, _stricmp against def->name (+0x40)");
fn(0x00591e30L, "Game::WeaponDictionary", "FindByNameSorted", "__thiscall", "WeaponDef * FindByNameSorted(WeaponDictionary * this, char * name)", "binary search in the sorted vector; logs 'Weapon not found: \"%s\" - Was it added to the index file?'");
fn(0x00576f40L, "Game::SectionDictionary", "SectionDictionary", "__thiscall", "SectionDictionary * SectionDictionary(SectionDictionary * this, TechTree * tree)", "For species 0..6: opens <SpeciesDir>/_shipsections.txt with Script::ReadToken pairs (id = atoi(tok1), file = tok2), LoadSection(dir+\"/\"+file, species, id), push_back non-NULL results into this+8 vector. Then validates '@' localisation tokens ('Section name %s not localized.').");
fn(0x00576cd0L, "Game::SectionDictionary", "LoadSection", "__thiscall", "SectionDef * LoadSection(SectionDictionary * this, char * path, int species, int id)", "Script::Open(path); first block named \"shipsection\" -> new SectionDef(index, species, id) (0x3d8 bytes) + SectionDef::ParseScript; returns NULL on failure ('SectionDictionary: Couldn't load [%s]').");
fn(0x005744e0L, "Game::SectionDef", "ParseScript", "__thiscall", "bool ParseScript(SectionDef * this, Script * script, int * rc, TechTree * tree)", "Same shape as WeaponDef::ParseScript (87 keys + bank/mount/thruster/option/optiondef/anim/netforcelimits sub-blocks, all _stricmp).");
fn(0x00574020L, "Game::SectionDef", "SectionDef", "__thiscall", "SectionDef * SectionDef(SectionDef * this, int index, int species, int id)", null);
fn(0x00545ec0L, "Game::Species", "GetDirName", "__cdecl", "char * GetDirName(uint species)", "species 0..6 -> Species/<Race> directory name (table 0x00b10a00 stride 0x184, name at +0xb8)");
fn(0x0058b870L, "Game::MasterTechTree", "MasterTechTree", "__thiscall", "MasterTechTree * MasterTechTree(MasterTechTree * this)", "Builds the tech list from TechTree/MasterTechList.tech (vector<string> of files, each via LoadTechFile(path, 1)), then sorts/links techs.");
fn(0x0058b770L, "Game::MasterTechTree", "LoadTechFile", "__thiscall", "void LoadTechFile(MasterTechTree * this, char * path, int flag)", "Script::Open(path); every top-level block named \"tech\" -> ParseTech(script, flag); other blocks SkipBlock(1).");
fn(0x0058b050L, "Game::MasterTechTree", "ParseTech", "__thiscall", "void ParseTech(MasterTechTree * this, Script * script, int flag)", "Parses one tech{} block (name/family/type/threat/requires/allows/strategy/ship/weapon) into a new TechDef; see strategic-turn-internals.md 2.5");
fn(0x00899280L, "Game::DemoApp", "LoadGameData", "__thiscall", "void LoadGameData(DemoApp * this)", "new MasterTechTree -> this+0x110; new WeaponDictionary (ctor 0x005917e0) -> +0x118 then WeaponDictionary::Init; new SectionDictionary(tree) -> +0x114; 0x00598ff0 -> +0x174; then cross-checks section bank weapons ('%s specifies weapon %s, but weapon not found.').");
fn(0x00899870L, "Game::DemoApp", "EnsureGameData", "__thiscall", "void EnsureGameData(DemoApp * this)", "Once (flag this+0x10d): LoadGameData(); 0x004d7790 (GUI scripts via ConfigParser::ParseFile); creates StrategyAIContext. Called from DemoApp::OnTick when this+0x10c is set (deferred data load after the first frame).");
// ---------- M4: gobio ----------
log.println("== M4 gobio");
fn(0x008d7010L, "Mars::gobio", "Init", "__cdecl", "void Init(StringVector * mountPaths)", "Once: constructs the FileSystemSet singleton (0x00b2e274) from the mount list and sets g_gobio. Called from Application::MountModules.");
fn(0x008d6d60L, "Mars::gobio::FileSystemSet", "FileSystemSet", "__thiscall", "FileSystemSet * FileSystemSet(FileSystemSet * this, StringVector * mounts)", "If no mount equals \".\" / \"./\" / \".\\\", a NativeFileSystem(\"\") (current directory) is added FIRST. Then each mount in order: \".\" -> NativeFileSystem(\"\"); PathIsDirectoryA(path) -> NativeFileSystem(path); else ZipFileSystem(path) (.gob). Open() searches in this order => loose files override .gob contents.");
fn(0x008d5060L, "Mars::gobio::FileSystemSet", "Open", "__thiscall", "bool Open(FileSystemSet * this, char * path)", "Close current; first IFileSystem whose vft[1] Open(path) succeeds becomes this->current.");
fn(0x008d7090L, "Mars::gobio::FileSystemSet", "ListFiles", "__thiscall", "void ListFiles(FileSystemSet * this, char * pattern, StringVector * out)", "Union over all file systems (vft[5]) into a set, then out vector.");
fn(0x008d5140L, "Mars::gobio", "ReadFile", "__cdecl", "bool ReadFile(char * path, IBuffer * * out)", "THE 'read whole file by relative path' entry: g_gobio->Open(path); size = fs->Size(); Buffer::Create(size) ; fs->Read(data,size) == size ? *out = buffer : release; fs->Close(). Returns true on success. Used by Script::Open, TextFileStream::Open, textures, sounds, etc.");
fn(0x008d50d0L, "Mars::gobio", "Open", "__cdecl", "bool Open(char * path)", "g_gobio->Open(path)");
fn(0x008d4d20L, "Mars::gobio", "Close", "__cdecl", "void Close(void)", null);
fn(0x008d4d50L, "Mars::gobio", "Size", "__cdecl", "uint Size(void)", "size of the currently open file");
fn(0x008d4d80L, "Mars::gobio", "Read", "__cdecl", "uint Read(void * dst, uint n)", "read from the currently open file");
fn(0x008d50f0L, "Mars::gobio", "Exists", "__cdecl", "bool Exists(char * path)", "any file system vft[6] Exists(path)");
fn(0x008d71d0L, "Mars::gobio", "ListFiles", "__cdecl", "void ListFiles(char * pattern, StringVector * out)", "g_gobio->ListFiles");
fn(0x008d4c90L, "Mars::gobio::Buffer", "Create", null, null, "allocates a gobio::Buffer of EBX bytes (register convention: size in EBX, out IBuffer** on stack)");
fn(0x008d4c80L, "Mars::gobio::Buffer", "Size", "__thiscall", "uint Size(IBuffer * this)", null);
fn(0x00682d50L, "Mars::gobio::Buffer", "Data", "__thiscall", "void * Data(IBuffer * this)", null);
fn(0x008d57a0L, "Mars::gobio::NativeFileSystem", "NativeFileSystem", "__thiscall", "NativeFileSystem * NativeFileSystem(NativeFileSystem * this, char * root)", "'Mounting current directory...' / 'Mounting subdirectory %s...'; root must be a directory; a trailing slash is appended");
fn(0x008d4ef0L, "Mars::gobio::NativeFileSystem", "Open", "__thiscall", "bool Open(NativeFileSystem * this, char * path)", "fopen(root+path, \"rb\") (vft[1])");
fn(0x008d4ba0L, "Mars::gobio::NativeFileSystem", "Close", "__thiscall", "void Close(NativeFileSystem * this)", "vft[2]");
fn(0x008d4bc0L, "Mars::gobio::NativeFileSystem", "Size", "__thiscall", "uint Size(NativeFileSystem * this)", "vft[3] ftell/fseek");
fn(0x008d4c30L, "Mars::gobio::NativeFileSystem", "Read", "__thiscall", "uint Read(NativeFileSystem * this, void * dst, uint n)", "vft[4] fread");
fn(0x008d6730L, "Mars::gobio::NativeFileSystem", "ListFiles", "__thiscall", "void ListFiles(NativeFileSystem * this, char * pattern, void * outSet)", "vft[5] _findfirst/_findnext");
fn(0x008d4f60L, "Mars::gobio::NativeFileSystem", "Exists", "__thiscall", "bool Exists(NativeFileSystem * this, char * path)", "vft[6]");
fn(0x008d4e90L, "Mars::gobio::NativeFileSystem", "MakePath", "__thiscall", "void MakePath(NativeFileSystem * this, char * rel, char * out260)", "_snprintf(\"%s%s\", root, rel)");
fn(0x008d6c40L, "Mars::gobio::ZipFileSystem", "ZipFileSystem", "__thiscall", "ZipFileSystem * ZipFileSystem(ZipFileSystem * this, char * zipPath)", "opens the .gob (zip) and indexes its entries into a map ordered by PathCompareCI");
fn(0x008d56a0L, "Mars::gobio::ZipFileSystem", "Open", "__thiscall", "bool Open(ZipFileSystem * this, char * path)", "vft[1]: Locate(path) then unzOpenCurrentFile");
fn(0x008d4af0L, "Mars::gobio::ZipFileSystem", "Close", "__thiscall", "void Close(ZipFileSystem * this)", "vft[2]");
fn(0x008d4b00L, "Mars::gobio::ZipFileSystem", "Size", "__thiscall", "uint Size(ZipFileSystem * this)", "vft[3]");
fn(0x008d4b40L, "Mars::gobio::ZipFileSystem", "Read", "__thiscall", "uint Read(ZipFileSystem * this, void * dst, uint n)", "vft[4] unzReadCurrentFile loop");
fn(0x008d64d0L, "Mars::gobio::ZipFileSystem", "ListFiles", "__thiscall", "void ListFiles(ZipFileSystem * this, char * pattern, void * outSet)", "vft[5] PathMatchSpecA over entries");
fn(0x008d56e0L, "Mars::gobio::ZipFileSystem", "Exists", "__thiscall", "bool Exists(ZipFileSystem * this, char * path)", "vft[6]");
fn(0x008d55c0L, "Mars::gobio::ZipFileSystem", "Locate", "__thiscall", "bool Locate(ZipFileSystem * this, char * path)", "map lookup with PathCompareCI, then unzGoToFilePos");
fn(0x008d4a50L, "Mars::gobio", "PathCompareCI", null, null, "register-convention strcmp variant: '\\\\' == '/', ASCII case-folded => zip entry names are case- and slash-insensitive");
fn(0x008a0a20L, "Mars::Application", "MountModules", "__cdecl", "void MountModules(void)", "Reads sots.ini [Modules] (GetPrivateProfileSectionA): keys Mount<N> = path, sorted by N; if none, uses the built-in table g_DefaultMountTable (sots_local_en.gob, sots.gob; count at 0x00b2d514); then gobio::Init(paths). Called from Application::Initialize before GlobalConsts::LoadAll.");
fn(0x008991c0L, "Game", "GetDefaultMounts", "__cdecl", "void GetDefaultMounts(int * count, char * * * table)", "*count = 3 (0x00b2d514), *table = g_DefaultMountTable (0x00a35e98)");
glob(0x00b2e270L, "g_gobio", "FileSystemSet*", "pointer to the FileSystemSet singleton (NULL before gobio::Init)");
glob(0x00b2e274L, "g_gobioInstance", "FileSystemSet", null);
glob(0x00a35e98L, "g_DefaultMountTable", "ptr", "const char* table used when sots.ini has no [Modules] Mount entries");
// ---------- effect / TextFileStream ----------
log.println("== TextFileStream");
fn(0x008cfcb0L, "Mars::Stream", "OpenFile", "__cdecl", "Stream * OpenFile(char * path)", "Factory: sniffs the file (0x008cf940: 'TXT' magic via gobio) and returns a TextFileStream opened for reading, else another FileStream flavour; NULL on failure.");
fn(0x0091e460L, "Mars::TextFileStream", "TextFileStream", "__thiscall", "TextFileStream * TextFileStream(TextFileStream * this)", null);
fn(0x008cfaa0L, "Mars::FileStream", "FileStream", "__thiscall", "Stream * FileStream(Stream * this)", null);
fn(0x008cfb90L, "Mars::TextFileStream", "Open", "__thiscall", "bool Open(TextFileStream * this, char * path, int mode)", "mode must be 1 (read): gobio::ReadFile(path,&buf); requires the buffer to start with the magic 'TXT' (PTR 0x00af6718) then skips the line break(s). Returns true on success.");
fn(0x008cfb00L, "Mars::TextFileStream", "Create", "__thiscall", "bool Create(TextFileStream * this, char * path, int mode)", "mode 2: fopen(path,\"wb\") and writes the 'TXT' magic line");
fn(0x008cfa30L, "Mars::FileStream", "ReadLine", "__thiscall", "uint ReadLine(Stream * this, char * out, int max)", "copies up to and including \\n from buf+cursor, NUL-terminates over the \\n, advances cursor; returns bytes consumed");
fn(0x0091e760L, "Mars::TextFileStream", "MatchLabel", "__thiscall", "char * MatchLabel(TextFileStream * this, char * line, char * label)", "First word == \"END\" -> NULL. label NULL -> accept. Else skip leading whitespace, label must match exactly (case-sensitive) and be followed by whitespace/EOL; returns pointer to the value text; on mismatch logs 'TextFileStream: missing label: %s' once and returns NULL (caller pushes the line back).");
fn(0x0091ec90L, "Mars::TextFileStream", "ReadString", "__thiscall", "bool ReadString(TextFileStream * this, char * label, char * out, int outMax)", "vft[1]: value is the text between the first pair of double quotes, with backslash escapes (\\n \\t \\r \\b \\f \\v, else literal next char)");
fn(0x0091eba0L, "Mars::TextFileStream", "ReadBool", "__thiscall", "bool ReadBool(TextFileStream * this, char * label, bool * out)", "vft[2]: first char 0/F/f -> false, 1/T/t -> true, anything else leaves *out unchanged");
fn(0x0091eb30L, "Mars::TextFileStream", "ReadFloat", "__thiscall", "bool ReadFloat(TextFileStream * this, char * label, float * out)", "vft[3]: sscanf %f");
fn(0x0091eab0L, "Mars::TextFileStream", "ReadInt", "__thiscall", "bool ReadInt(TextFileStream * this, char * label, int * out, int dflt)", "vft[4]: sscanf %d, *out = 0 if it does not parse");
fn(0x0091e8e0L, "Mars::TextFileStream", "ReadNested", "__thiscall", "void ReadNested(TextFileStream * this, char * label, IStreamable * obj)", "vft[5]: label line, then BEGIN ... END; obj NULL -> skip the block counting nested BEGIN/END; else obj->vft[1](stream) reads the body");
fn(0x0091ee80L, "Mars::TextFileStream", "WriteString", "__thiscall", "void WriteString(TextFileStream * this, char * label, char * value)", "vft[6]");
fn(0x0091e6c0L, "Mars::TextFileStream", "WriteBool", "__thiscall", "void WriteBool(TextFileStream * this, char * label, bool value)", "vft[7]: 'LABEL TRUE|FALSE'");
fn(0x0091e630L, "Mars::TextFileStream", "WriteFloat", "__thiscall", "void WriteFloat(TextFileStream * this, char * label, float value)", "vft[8]: 'LABEL %f'");
fn(0x0091e5a0L, "Mars::TextFileStream", "WriteInt", "__thiscall", "void WriteInt(TextFileStream * this, char * label, int value)", "vft[9]: 'LABEL %d'");
fn(0x0091e4c0L, "Mars::TextFileStream", "WriteNested", "__thiscall", "void WriteNested(TextFileStream * this, char * label, IStreamable * obj)", "vft[10]: label line, BEGIN, indent++, obj->vft[2](stream), indent--, END (tab-indented)");
fn(0x008db650L, "Mars::ParticleSystem", "Read", "__thiscall", "void Read(ParticleSystem * this, Stream * s)", ".effect body reader: NAME, ALIGNTOMOTION, FORCESINGLE, USEWORLDSPACE, MOMXFER, MESHFILENAME, TEXTUREFILENAME, SHADERFILENAME, ..., RANDOMIZESEED, BACKTOFRONTDRAW, PENNANTTRAIL, CREATIONRATE{BEGIN..END}, COLOR_R/G/B/A, NUMINITIALPARTICLES, NUM_TRACKS then per track PARTICLEDATATYPE + CREATION/VARIATION/OVERLIFE channels, NUM_ATTRACTORS/ATTRACTORINDEX. Order is significant; missing labels keep defaults.");
fn(0x008dad60L, "Mars::ParticleSystem", "Write", "__thiscall", "void Write(ParticleSystem * this, Stream * s)", "mirror of Read (label order is the file order)");
fn(0x008b42b0L, "Mars::EffectDictionary", "Load", "__thiscall", "void * Load(EffectDictionary * this, char * name)", "probable: builds \"effects/\"+name(.effect), Stream::OpenFile, allocates 0x1c0-byte effect and reads it (ParticleSystem::Read via helper). Cached in this+0x108 vector.");
// ---------- Application spine (coordinator request) ----------
log.println("== Application");
fn(0x008a0e50L, "Mars::Application", "Initialize", "__thiscall", "bool Initialize(Application * this, AppStartup * startup)", "VERIFIED: __thiscall, ONE stack argument (AppStartup* built in WinMain: {HINSTANCE hInst; std::vector<std::string> args; ...; int nShow; const char* wndClass=\"Kerberos_SwordOfTheStars_WndCls\"; int iconId=0x66}), callee pops 4 (RET 4), returns bool in AL (WinMain: if (Initialize(&startup)) Run()). EDX is not an input. A detour must be declared with the extra parameter or it will unbalance the stack.");
fn(0x0089f5b0L, "Mars::Application", "Run", "__thiscall", "void Run(Application * this)", "VERIFIED: __thiscall, no stack arguments (RET), EDX not read before being clobbered; no meaningful return value (EAX = PumpMessages result).");
fn(0x0089a640L, "Game::DemoApp", "OnTick", "__thiscall", "bool OnTick(DemoApp * this)", "VERIFIED: __thiscall, no stack arguments, returns bool in AL: 0 when quitting (this+0x1b2 set), else 1. IApplication vft+0x1c.");
log.println("== verify");
verifyDump();
log.close(); decomp.dispose(); println("done");
}
}