import ghidra.app.script.GhidraScript; import ghidra.program.model.address.*; import ghidra.program.model.mem.*; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.*; import ghidra.program.model.scalar.*; import ghidra.program.model.lang.*; import ghidra.program.model.data.*; import ghidra.app.decompiler.*; import java.util.*; import java.util.regex.*; import java.io.*; // Pass 4: remaining decompiles (+0x228 producers, WriteString for std::string layout) and write-back of names/comments/labels. public class TechFx4 extends GhidraScript { DecompInterface decomp; Memory mem; ReferenceManager rm; static final String OUT = "/tmp/techfx4"; List names = new ArrayList<>(); String cstr(Address a) { try { byte[] b = new byte[80]; mem.getBytes(a, b); int i = 0; while (i < 80 && b[i] != 0 && (b[i]&0xff) >= 0x20 && (b[i]&0xff) < 0x7f) i++; if (i >= 1 && i < 80 && b[i] == 0) return new String(b, 0, i, "ISO-8859-1"); } catch (Exception e) {} return null; } String techName(long v) { return (v >= 10000 && v <= 10195) ? names.get((int)(v - 10000)) : (v == 0xc5 ? "NONE" : null); } String constAt(long a) { try { Address ad = toAddr(a); int iv = mem.getInt(ad); long lv = mem.getLong(ad); float f = Float.intBitsToFloat(iv); double d = Double.longBitsToDouble(lv); String s = cstr(ad); if (s != null) return "\"" + s + "\""; StringBuilder sb = new StringBuilder(); sb.append("i=" + iv); String tn = techName(iv & 0xffffffffL); if (tn != null) sb.append("(" + tn + ")"); if (!Float.isNaN(f) && Math.abs(f) < 1e12 && (Math.abs(f) > 1e-7 || f == 0)) sb.append(" f=" + f); if (!Double.isNaN(d) && Math.abs(d) < 1e12 && (Math.abs(d) > 1e-7 || d == 0)) sb.append(" d=" + d); return sb.toString(); } catch (Exception e) { return "?"; } } String subst(String c) { Matcher m = Pattern.compile("&?(DAT|PTR_s_|s_[A-Za-z0-9_]*|PTR_DAT|u_[A-Za-z0-9_]*|_DAT|PTR_PTR|_PTR)_([0-9a-f]{8})").matcher(c); StringBuffer sb = new StringBuffer(); while (m.find()) { long a = Long.parseLong(m.group(2), 16); Address ad = toAddr(a); String s = cstr(ad); String rep = m.group(0); if (s != null && s.length() <= 64 && !m.group(1).equals("PTR_DAT")) rep = "\"" + s + "\""; else if (a >= 0x9dd000L && a < 0xb40000L) { rep = m.group(0) + "/*" + constAt(a) + "*/"; } m.appendReplacement(sb, Matcher.quoteReplacement(rep)); } m.appendTail(sb); Matcher m2 = Pattern.compile("0x27([0-9a-f]{2})\\b").matcher(sb.toString()); StringBuffer sb2 = new StringBuffer(); while (m2.find()) { long v = Long.parseLong("27" + m2.group(1), 16); String tn = techName(v); m2.appendReplacement(sb2, Matcher.quoteReplacement(m2.group(0) + (tn != null ? "/*" + tn + "*/" : ""))); } m2.appendTail(sb2); return sb2.toString(); } String decompRaw(Function f) { try { DecompileResults res = decomp.decompileFunction(f, 600, monitor); if (res == null || !res.decompileCompleted()) return "[decompile failed]"; return res.getDecompiledFunction().getC(); } catch (Exception e) { return "[exception " + e.getMessage() + "]"; } } Set done = new HashSet<>(); void dumpFunc(Function f, PrintWriter idx, boolean disasm) throws Exception { if (f == null || done.contains(f.getEntryPoint().getOffset())) return; done.add(f.getEntryPoint().getOffset()); String t = String.format("%08x", f.getEntryPoint().getOffset()); PrintWriter w = new PrintWriter(new FileWriter(OUT + "/" + t + ".c")); w.println("// " + f.getName(true) + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); StringBuilder calls = new StringBuilder(); for (Function cf : f.getCalledFunctions(monitor)) calls.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); w.println("// CALLS: " + calls); StringBuilder callers = new StringBuilder(); for (Function cf : f.getCallingFunctions(monitor)) callers.append(cf.getName(true) + "@" + cf.getEntryPoint() + " "); w.println("// CALLERS: " + callers); w.println(subst(decompRaw(f))); if (disasm) { w.println("// ---- DISASM ----"); Listing l = currentProgram.getListing(); InstructionIterator ii = l.getInstructions(f.getBody(), true); while (ii.hasNext()) { Instruction in = ii.next(); StringBuilder ann = new StringBuilder(); for (Reference r : in.getReferencesFrom()) { long ta = r.getToAddress().getOffset(); if (ta >= 0x9dd000L && ta < 0xb40000L) { Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(r.getToAddress()); ann.append(" ; " + (s != null ? s.getName() : String.format("%08x", ta)) + "=" + constAt(ta)); } else if (r.getReferenceType().isCall()) { Function cf = getFunctionAt(r.getToAddress()); if (cf != null) ann.append(" ; ->" + cf.getName(true)); } } w.println(String.format("%08x %-40s%s", in.getAddress().getOffset(), in.toString(), ann)); } } w.close(); idx.println("decompiled " + t + " " + f.getName(true) + " size=" + f.getBody().getNumAddresses()); idx.flush(); } void rename(long a, String ns, String name, String comment) throws Exception { Function f = getFunctionAt(toAddr(a)); if (f == null) { println("no function at " + Long.toHexString(a)); return; } Namespace n = currentProgram.getGlobalNamespace(); if (ns != null) for (String part : ns.split("::")) n = currentProgram.getSymbolTable().getOrCreateNameSpace(n, part, SourceType.USER_DEFINED); if (f.getName().startsWith("FUN_")) { f.setName(name, SourceType.USER_DEFINED); f.setParentNamespace(n); } if (comment != null) { String old = f.getComment(); f.setComment((old == null || old.isEmpty()) ? comment : old + "\n" + comment); } } void label(long a, String name, String comment) throws Exception { Address ad = toAddr(a); Symbol s = currentProgram.getSymbolTable().getPrimarySymbol(ad); if (s == null || s.getName().startsWith("DAT_") || s.getName().startsWith("PTR_")) currentProgram.getSymbolTable().createLabel(ad, name, SourceType.USER_DEFINED).setPrimary(); if (comment != null) currentProgram.getListing().setComment(ad, CodeUnit.PLATE_COMMENT, comment); } @Override public void run() throws Exception { mem = currentProgram.getMemory(); rm = currentProgram.getReferenceManager(); decomp = new DecompInterface(); decomp.openProgram(currentProgram); new File(OUT).mkdirs(); PrintWriter idx = new PrintWriter(new FileWriter(OUT + "/index.txt")); for (int i = 0; i < 196; i++) { long a = 0x009ff9e4L + 8*i; int p = mem.getInt(toAddr(a)); String s = (p > 0x9dd000 && p < 0xad9000) ? cstr(toAddr(p & 0xffffffffL)) : null; names.add(s == null ? "?" : s); } PrintWriter rep = new PrintWriter(new FileWriter(OUT + "/report.txt")); // 1. decompiles String[] want = {"007cf560","0077b620","00747ba0","0053be20","00581cc0","00747d30","0074aff0","00745aa0","0080dcb0","0080ddb0","0080dde0","0080af60","0080ba90","008102f0","0081bf50","0080baf0","0080bb40","0080bb60","0080d080","0087fac0","008822c0","0080b730","0081e9d0","008672c0","007870d0"}; String[] dis = {"007cf560","00747ba0","0053be20","00581cc0","0080dcb0","0080ddb0","0080dde0","0080af60","0080ba90","0080baf0","0080bb40","0080bb60","0080d080","0080b730"}; Set disSet = new HashSet<>(Arrays.asList(dis)); for (String t : want) { Function f = getFunctionAt(toAddr(Long.parseLong(t, 16))); if (f != null && f.getBody().getNumAddresses() < 30000) dumpFunc(f, idx, disSet.contains(t) || f.getBody().getNumAddresses() < 600); } // WriteString candidates: functions whose name mentions Stream and Write/String rep.println("== Stream write/string functions =="); FunctionIterator fi = currentProgram.getListing().getFunctions(true); while (fi.hasNext()) { Function f = fi.next(); String n = f.getName(true); if ((n.contains("Stream") && (n.contains("Write") || n.contains("String"))) || n.contains("WriteString") || n.contains("ReadString")) { rep.println(" " + n + " @ " + f.getEntryPoint() + " size=" + f.getBody().getNumAddresses()); if (f.getBody().getNumAddresses() < 4000) dumpFunc(f, idx, true); } } // also the IStreamable vtable slot +0x18 of ServerPlayer sub-vftable 0x00a32794 rep.println("== IStreamable sub-vftable 0x00a32794 =="); for (int i = 0; i < 8; i++) { int p = mem.getInt(toAddr(0x00a32794L + 4*i)); Function f = getFunctionAt(toAddr(p & 0xffffffffL)); rep.println(String.format(" slot %d: %08x %s", i, p, f == null ? "?" : f.getName(true) + " size=" + f.getBody().getNumAddresses())); } // Mars::Stream vtable guess: find callers of ServerPlayer::Write's string helper — dump ServerPlayer::Write's first 200 instructions calls Function wr = getFunctionAt(toAddr(0x008563e0L)); if (wr != null) { rep.println("== ServerPlayer::Write callees =="); Map cnt = new TreeMap<>(); for (Function cf : wr.getCalledFunctions(monitor)) cnt.put(cf.getName(true) + "@" + cf.getEntryPoint() + " sz=" + cf.getBody().getNumAddresses(), 1); for (String k : cnt.keySet()) rep.println(" " + k); for (Function cf : wr.getCalledFunctions(monitor)) { long sz = cf.getBody().getNumAddresses(); if (sz > 40 && sz < 700) { // look for functions that test [x+0x14] >= 0x10 (SSO) InstructionIterator ii = currentProgram.getListing().getInstructions(cf.getBody(), true); boolean sso = false; while (ii.hasNext()) { Instruction in = ii.next(); String s = in.toString(); if ((s.contains("0x14]") || s.contains("0x14 ]")) && (s.startsWith("CMP"))) sso = true; } if (sso) { rep.println(" SSO-candidate " + cf.getName(true) + "@" + cf.getEntryPoint()); dumpFunc(cf, idx, true); } } } } // 2. write-back int tx = currentProgram.startTransaction("tech-effects writeback"); try { rename(0x00891790L, "Game::ServerPlayer", "OnTechResearched", "ServerPlayer::OnTechResearched(TechDef* def, bool silent) - vft slot 4, called from TechTree::SetResearched. Hard-coded strategic tech effects: chain of MasterTechTree::IsTech(def, 10000+i) tests writing ServerPlayer fields (ConMod/SavMod/OutMod/PopMod/TerraMod/SuitTol/MaxOH/MinRate/pddm/CstR-E-T/PrGtTrf/flags). See /srv/re-lab/handoff/tech-effects.md for the id->effect table. Tail: plague-cure mask (HasImm/HasVac), tech bitmasks (+0x190/+0x194), RebuildSpeciesTechFlags, Zuul CruisCon->free BrdPod, cdp=SpyBm&&SlvgTech, EVENT_TEMPERANCE cure."); rename(0x0057d5d0L, "Game::MasterTechTree", "IsTech", "bool IsTech(TechDef* def, int techId): techId = 10000+index into g_TechIdNames (0x009ff9e4, 196 entries); 0xc5 = none. Compares def against the resolved TechDef*[196] at *this."); rename(0x0057d610L, "Game::MasterTechTree", "GetTechDef", "TechDef* GetTechDef(int techId 10000..10195) from the resolved table at *this; 0 if none/0xc5."); rename(0x0057d810L, "Game::TechTree", "HasResearched", "bool HasResearched(int techId): master->resolved[techId-10000] -> node state == 4."); rename(0x00581c10L, "Game::MasterTechTree", "ResolveTechIds", "Fills this->resolved[196] (TechDef*) by _stricmp-looking up each name of g_TechIdNames (0x009ff9e4, stride 8) in the sorted tech list. Called at the end of the MasterTechTree ctor."); rename(0x006965c0L, null, "IsCombatTechName", "Returns 1 if the string (std::string at param+4) matches any of the 116 names in g_CombatTechNames (0x00a19718). Pure membership test (weapon/section tech families for the design/combat side); not the effects table."); rename(0x0082bf10L, "Game::ServerPlayer", "RebuildSpeciesTechFlags", "Recomputes flags[7] at +0x348 from the tech tree: for species i, speciesdef(i)+0x78..+0x9c hold tech ids (translation1/2/3, incorporate, addict, temperance, subjugate, accommodate, proliferate order per SpeciesDef init) -> bits 0..8 = HasResearched(id). Called after every research completion and on load."); rename(0x00747ae0L, "Game::ServerSystem", "HazardMod", "double HazardMod(suit, ideal, tol) = clamp01(1 - |suit-ideal| / (tol + 0.1)). Linear, no curve."); rename(0x00818600L, "Game::ServerPlayer", "UpdateBankruptcyLimits", "maxIncome = sum ServerSystem::ComputeOutputMax()[3]; BnkEl = max(ftol(maxIncome / -0.15), -2e9); BnkPr = max(-ftol(maxIncome * BANKRUPTCY_PROTECTION_LIMIT_FACTOR), BnkEl). The 3.3 factor is on the PROTECTION limit; elimination limit is -6.67x max income (15% debt interest = max income)."); rename(0x0080e260L, "Game::ServerPlayer", "SetBankruptcyState", "if (level != BnkWrn) { BnkWrn = level; BnkTrn = level ? ModCount : -1; } - stamp only on transitions; ProcessBankruptcy acts on the previous turn's (BnkWrn,BnkTrn)."); rename(0x0080db10L, "Game::ServerPlayer", "BankruptcyLevel", "2 if Sav < BnkEl, 1 if Sav < BnkPr, else 0."); rename(0x0080e330L, "Game::ServerPlayer", "ApplyAITechBonus", "For CCC_AI/CCC_AIAdmin/CCC_AIFac: v = techStrategyValue(def)->+8 * (AIBn ? +1 : -1); adds to ResMod / IncMod(+0x30c) / OutMod respectively. AIVrus/AISlv: no numeric effect here."); rename(0x008186b0L, "Game::ServerPlayer", "SetAIBenefit", "SetAIBenefit(bool on): flips AIBn (+0xfe) and re-applies the sign of every researched AI tech bonus (6-entry table at 0x00aea2f0)."); rename(0x00537240L, "Game::TechDef", "GetPlagueCureMask", "If def is (or descends from) BIO_PLGVAC/RTPLGVAC/BSTVAC/ASPLGVAC/CONNAN -> mask = 1< 0x0f. Used by OnTechResearched: HasImm |= mask, HasVac |= mask, cure plagues."); rename(0x0080e410L, "Game::ServerPlayer", "SpeciesOfTranslationTech", "Returns species index i whose speciesdef(i)+0x74 (CCC_NDTRKHUM/ZUL 'node track' tech) == def, else -1; OnTechResearched sets NPTrk |= 1<+4 - speciesdef.costFactor(+0x24) * min(|IdealSuit-Suit|, SuitTol) * 10000 * 1.5"); rename(0x007484d0L, "Game::ServerSystem", "CalcSuitMod", "min(|IdealSuit(species) - Suit|, owner.SuitTol); 0 if RebAI owner; 20 if no owner (logs)."); rename(0x0074d4f0L, "Game::ServerSystem", "AccrueSystemBonus", "Requires owner, IsStable, ModCount-TAcq > SYSTEMBONUS_MINTURNS, ntdev > MINTURNS. pbon += min(max(ftol(POPBONUS_INC*cap),0), max(ftol(POPBONUS*cap) - pbon, 0)); ibon += min(max(INFRABONUS_INC,0), max(INFRABONUS - ibon, 0)). Species with speciesdef+0x5c == 0 (Zuul) get no bonus."); rename(0x0074b5a0L, "Game::ServerSystem", "SystemBonusPopTarget", "ftol(max(frac,0) * MaxPop(imperial)) if speciesdef(owner)+0x5c else 0."); rename(0x00705510L, "Game::NodeLine", "Step", "Node-line travel: segments from FUN_00705280 (line clipped to each system's STUTTER_SYSTEM_INFLUENCE_RADIUS sphere); per segment speed = nodespeed * ((MAX-MIN) * dist(system, segment)/RADIUS + MIN); outside spheres full nodespeed."); rename(0x00705280L, "Game::NodeLine", "BuildStutterSegments", "Clips the travel line against every system's influence sphere (FUN_008a64f0 ray/sphere), keeps [t0,t1]*len entries, sorts, and merges overlaps at the midpoint. Guarantees dist <= RADIUS inside a segment (the implicit clamp)."); rename(0x008e8eb0L, null, "DistPointToSegment", "Distance from point to segment (t clamped to [0,1])."); rename(0x005453a0L, "Game::SpeciesDef", "InitTable", "Hard-coded per-species constants table at 0x00b10a00 (7 x 0x184): drive type, growth/hazard/income/cost factors, XNC tech ids at +0x78.., NDTRK tech at +0x74, sensor mod, colour. See tech-effects.md section 4."); rename(0x00545cc0L, "Game::SpeciesDef", "Get", "SpeciesDef* Get(int species 0..6) -> 0x00b10a00 + i*0x184."); rename(0x0080dd10L, "Game::ServerPlayer", "GetIncMod", null); rename(0x0080dd20L, "Game::ServerPlayer", "GetSpeciesCostFactor", "speciesdef(Species)+0x24 (Zuul 0.7, else 1.0)"); label(0x009ff9e4L, "g_TechIdNames", "TechId name table: 196 x {const char* name, int}; TechId = 10000 + index. Resolved per game into MasterTechTree resolved[] by ResolveTechIds. This is the table the hard-coded effects key on."); label(0x00a19718L, "g_CombatTechNames", "116 x const char* - membership list used by IsCombatTechName (0x006965c0); not an effects table."); label(0x00b10a00L, "g_SpeciesDefTable", "7 x SpeciesDef (0x184 B), filled by SpeciesDef::InitTable (0x005453a0)."); label(0x00adf378L, "g_TechBitmaskTableA", "32 x {techId, bit} -> ServerPlayer+0x190"); label(0x00adf478L, "g_TechBitmaskTableB", "29 x {techId, bit} -> ServerPlayer+0x194"); } finally { currentProgram.endTransaction(tx, true); } rep.close(); idx.close(); decomp.dispose(); println("done"); } }