sots-re/ghidra/scripts/TechFx4.java

140 lines
17 KiB
Java

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<String> 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<Long> 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<String> 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<String,Integer> 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<<k (k=0..4); BIO_UNIANTI -> 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<<i.");
rename(0x004d7960L, null, "ComputeTechBitmasks", "out[0] = OR of bit for each researched tech in table 0x00adf378 (32 x {techId,bit}); out[1] = same over 0x00adf478 (29 entries). Stored at ServerPlayer+0x190/+0x194 (design/section availability masks).");
rename(0x007505b0L, "Game::ServerSystem", "TradePointsToMoney", "money = ((trade - fmod(trade,5))*5 + PopIncome(imperial) + PopIncome(civilian) + SlaveIncome) * speciesdef.incomeFactor(+0x18) * owner.IncMod(+0x30c) * srv.IncomeMod(+0xbc)*DifficultyMods->+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");
}
}