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 types = new HashMap(); List verify = new ArrayList(); 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 ps = new ArrayList(); 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 in N order (default table: sots.gob, sots_local_en.gob)"); fields(fs, new Object[][]{ {0,"ptr","fs_begin","std::vector 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 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 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 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 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="edFlag, 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 /_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/ 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 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 = 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 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"); } }