Settles the 0x18-vs-0x1c contradiction lane X raised. 0x1c is right, everywhere, and there is exactly one std::string instantiation in this binary: _Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18. ObservedTech+0x24 is that string's trailing empty-allocator word, not the unaccounted data field it was read as. Three complete enumerations of the element each skip it: ObservedTech::Write 0x00817cf0, the ctor 0x008562a0, and the copy ctor inlined at 0x0079a184. Generalised with a new scanner, tools/strfootprint.py, which recovers every (base, disp, tag) handed to the Mars::Stream string helpers: 65 std::string members off a non-stack base across every serializer in the exe, ZERO with a sibling member inside the 0x1c span, and 51 of the 52 measurable inter-member gaps exactly 0x1c. Corroborated by the vector<string> walk stride (add esi,0x1c @0x00699c29), PostEvent's by-value strings at [ebp+8]/[ebp+0x24] with RET 0x4c, and MoraleEvent 0x50 = name@0x34 + 0x1c. Blast radius: zero recovered struct tables were wrong. Every string-bearing layout already used 0x1c spans and 0x1c gaps -- ServerPlayer::pswd @0x2dc..0x2f7, the row flagged for re-checking, included. Only prose carried the 0x18 number: the loader-prototypes conventions line, the GlobalConst_ParseString prototype, and the ObservedTech element table. struct-recovery S0 additionally had _Mysize/_Myres transposed (size@0x14, res@0x18) while every table in the same file used the correct offsets; fixed. ObservedTech's four on-disk fields are now mapped rather than guessed, by reading the serializer as lane X suggested: +0x04 uint16 otnF, +0x06 uint16 otnL, +0x08 bool odet (ONE BYTE, WriteBool), +0x0c std::string otch (0x1c), +0x28 int owith = 0x2c exactly. That matches save_reader.py's on-disk order already. Game:: ObservedWeapon (0x00817bc0/0x00817b10) is the same element with tag owep. Oracles unaffected and re-run: save_reader 36/36 and --strict exit 0 on all three real saves; state_checksum 38 tests OK, coverage PROVED byte-for-byte on turn1 and turn3. sots-engine wip/strings 32d3e36 syncs the header and corrects two stale "unpinned" comments: clean_room_check OK, host ctest 33/33. Standing rule this produced: never size a struct member from the offsets the code touches. This build's STL puts the empty allocator LAST in both string (0x1c) and vector (0x10), and an empty allocator is never loaded or stored, so a touch-scan undercounts by exactly 4 every time. Size from an enumeration instead.
14 KiB
14 KiB
Open questions
Each links to the finding that raised it. Promoted to backlog or closed by re-quartermaster.
- Compiler archive mismatch — imports say MSVCR100/MSVCP100 (VS2010), but Ghidra applied
windows_vs12_32datatypes (closest available). Confirm the exact toolchain from the PE Rich header; decide which datatype archive best fits. (from 01-fingerprint) - Battle-load root cause — inferred as single-threaded engine + core-count-sensitive code;
must be confirmed by a
ForceSingleCore=0vs=1diff once the main loop is mapped. (from 00-strategy) - Save-editor struct coverage — community editors cover players/tech/systems; fleet/combat state coverage may be partial. Treat as strong hints, verify each field against real saves.
- Lockstep determinism —
@SYNC ERROR DETECTED: Frame %dindicates a deterministic lockstep sim. Big for reimplementation + old-vs-new differential (determinism makes equivalence checkable frame-by-frame). Confirm scope. (from strings-and-config) - Mods override mechanism —
.gob=gobio::ZipFileSystemwith aNativeFileSystemloose-file override; this is how./Modswins over archived files. Verify load order. (from strings-and-config) - Battle-load root cause — the shipped fix is a blunt process-affinity pin (
FUN_0089ee70) with NO CPU-topology detection, so there is no adaptive branch. The real cause is emergent multi-core contention: which thread(s)? Investigate the 3CreateThreadsites (FUN_00902350,FUN_00736e30, startupFUN_008a0e50), the streaming-sound thread, and lockstep sync waits. (from ghidra-recon) - Save-struct contradictions (R1 vs R2) — field widths (
Abdn/Dstyd/ltisInt16 vs Int32;Bats2), R2'sOID = PID*16owner-handle claim, species id 4 (_NPCvs 'AI Rebellion'). Resolve against the binary's Streamable read code. (from save-editor-structs) - Unlabeled save blocks —
CdPlayer(unknown1..35), emptySimSystemDetailSpy, opaque ~2500 B RNG blob. Analyst targets once the Streamable readers are located. (from save-editor-structs) - Missing HUD scripts — exe references
GUI/Combat/CombatHUD.script,SensorHUD.script,NoHUD.scriptbut none ship in the gobs or loose; likely dev-only overrides via the gobio native-FS fallback. Confirm viaCombatScreenload path. Also:.scriptfiles are display configs, not widget layouts (corrects round-one note). (from ui-screen-map) - Tech
allowsdefault per-race % —tech_tree.jsonedges only carry races written in theallowsstring; the default for an unlisted race (believed 100%) is engine code. Ghidra target in the tech loader. (from data-parsers) - Engine parser leniency — 12 shipped shipsections are syntactically broken (unclosed
{, extra}) yet load; keys and identifiers are case-insensitive. Reimplementation must match this leniency..effectis its ownTXT/BEGIN-ENDformat, not brace-block (corrects round one). (from data-parsers) - Struct recovery leftovers —
PlayerColorIDexact on-disk width (writerFUN_008b9cb0undecompiled);ServerSystem+0x10owner type;TechTreeper-tech body;CdPlayerblock. R2'sOID = PID*16is an id-allocation pattern, not in this code. (from struct-recovery) - Resolved (R1/R2 contradictions) —
Bats2/rcexare int64 (R2 wrong);Abdn/Dstydbools,ltisint;TRM/CstR/E/T/shrm/RefCap/RepCap/PlayerViewInfraare floats;pswdstring;TShn/ETS/diplomacy counters int16 in memory, int32 on disk;Nexpcarriesxid/xmin/xmax/xper;FtOrigis Vector3. (from struct-recovery) - Battle-load, narrowed — sim/combat load run on the main thread; the only other threads are net watchdog, TIME_CRITICAL audio streaming (
g_musicCS), and a star-map mesh builder. Hypothesis: audio-thread critical-section contention or D3D9 runtime/driver threads on many cores. Needs a dynamic profile (x32dbg / ETW) under the software-GPU stack. (from turn-spine) - Spine leftovers —
StrategyServerstruct partial (41 fields); static-initialiser region 0x009be000–0x009c1400 undisassembled;Mars::Streamvftable not located; several small ProcessTurn phase fns unnamed. (from turn-spine) - Save framing ambiguities (settle on first real save) — padding joint
[len][name][value][pad]vs split; bool vs int for names with len%4==0 (no type byte); on-disk tags for Summary/CreateParameters and count/element tags inside framed arrays unknown (positional for now). A real 3-char bool tag (NPC,Dep,hsp) settles padding. (from SAVE_FORMAT) - Strict-parse gaps vs real save (verifier output, round 3) — (1)
ServerSystem: 7/28 systems carryVFlagswhereName/vnhwere expected — a conditional layout (uncolonized/special systems?); (2)halt[].haltvis a 31/35-byte struct, not a bool; (3) optionalindi(IndependenceInfo?) precedesNVE/PIDin some systems; (4)PrisonerHold.prisoners[]entries lackPrNSp; (5)/createParams/keyis int; (6)turnstats.hist.stats.tchis int; (7) optional trailingzdsi/zdst. Padding = joint (settled). 7 resyncs / 2728 raw bytes ≈ RNG blob. (from turn2-strict-issues) - RESOLVED: strict-parse gaps — 5/8 were reader bugs (ASCII plausibility test applied to string values; empty string == int 0), 3 real:
PrNSponly whenPrMax>0;indiunconditional inNVOnodes (system-level gated byhindi);zdsi/zdstpairs underzdsc.Keyis an empty string;tchint. Summary/CreateParams tag lists confirmed. Reader patch in flight. (from schema-gaps-resolved) - RESOLVED: save framing — padding is joint;
"."= NULL tag (VectorHelper count+elements, Vector3 bodies); empty strings are 4 zero bytes;CDTis a tagged frame + opaqueCDframes. All three real saves parse--strictclean. Note:SvSctObIS present in all saves (patch doc §7 was wrong). (from SAVE_FORMAT) - (parked) Combat loader job API — does tactical-battle loading go through the job helpers
0x8fc160/0x8fa5b0with theTryEnterCriticalSection→Sleep(1000)completion poll (0x0071ea60)? If yes, that is a plausible many-core stall (lost try-lock = +1 s per poll). End Turn also has a deliberateSleep(computed)≈0.85 s at0x00838da9. (from battle-load-profile) - Ship-design code rules (Ghidra, from SHIP_DESIGN_RULES.md §8) — hidden default rider designs are built WITHOUT tech gating (where?); is section class-equality enforced or merely unoffered; confirm the turret class-accept table (
standard ← {standard,missile,grapple},strafe ← {standard}; grapple weakest); hull-class tech injection (IND_CruisCon/IND_DreadCon) + station exemption; option defaults/auto-upgrade and whatoption_costmultiplies; weapon cost per bank vs per mount; mass/speed formulas;faiDes/dWepmeaning. (from SHIP_DESIGN_RULES) - Corrections from turn internals —
ServerPlayer+0xf9is the AI flag (not bTurnDone);FUN_007d7f70is encounter detection; ProcessTurn phase 2 is trade, not diplomacy; the spine'sFUN_00840fe0'income' is special projects — income isFUN_00863030ComputeBudget.TECHBEN_*data blocks are inert (effects hard-coded via the 116-entry tech-name table @0x00a19718). Open: TRA/TRP producers,FPsp2derivation,PERGATETRAFFIC_*readers, exact BnkEl/BnkPr expression. (from strategic-turn-internals) Mars::Application::Initializesignature (P2-M0 finding) — a C++__thiscalldetour that called the original crashed inside Initialize; the trace hook now uses a convention-agnostic asm stub. Verify the real signature (stack args? EDX use? return value?) before anycompare/replacehook on it. Same caution for every[unverified]entry inghidra/addresses.json. (from m0)- Formula gaps surfaced by the game/sim port (feed back to Ghidra) — which bankruptcy limit carries the 3.3 factor and when the bankruptcy start turn is stamped; the suitability→carrying-capacity hazard curve shape; the trade-points→money system-income tail; the
POPBONUS_INCpopulation increment; the expense-slider request term; which running total the tech-income bonus / savings aid read; node-line speed clamp at the influence radius; whetherDecayAllResearchalso hits the current target. (from game-sim) - RESOLVED: Initialize signature —
bool __thiscall Application::Initialize(this, AppStartup*), RET 4; Run =void(this); OnTick =bool(this). Plain thiscall wrappers are safe for Run/OnTick; Initialize needs the extra arg. (from loader-prototypes) - PARITY BUG (fix in mars/text + Python flat_kv): duplicate config keys are FIRST-occurrence-wins in the engine (entry erased on use; duplicates log 'multiply defined'); our readers implement last-wins. Also the brace parser DROPS a final key/value pair that lacks a trailing newline, and uses whitespace-only delimiting (braces not delimiters) with `" ' `` quotes and no escapes — mars/parse must match these for compare mode; verify on the 12 broken sections + a no-trailing-newline fixture. (from loader-prototypes)
- Not traced end-to-end —
Species/_NPC/weapons/*.weaponloading and the.effectdictionary entryEffectDictionary_Load(0x008b42b0, unverified). (from loader-prototypes) - RESOLVED: End-Turn oracle — deterministic and cross-process: compare autosaves by sha256, no masking. Only when diffing a re-save of a loaded post-turn autosave: mask
Player.Status(→0) and recompute/ignoreSummary.Checksum(additive). (from determinism-oracle) - SAVE_FORMAT tag corrections (fix Python reader + spec) — real on-disk tags:
otnF(notontF) in Odes/Owep/Otch,nextid(notnextId) in NdGr2, Design =FAIDes/DHide/DWep/DName;ords/wptsare real tags. Python's positional R() matching hid these. RNG: float mapping(float)(y*2^-32),next_intmask, and lazy-vs-eager twist atleft==0still need binary confirmation. (from mars-stream) - RESOLVED: tech
allowsdefault — unlisted species = 1.0 (confirmed inFUN_005822d0tree-creation roll, strategic-turn-internals §2);game/datauses 100. Still open from game/data: what the engine's converter does with the 34 malformed tokens (force_right o,crew false,1.0f,0-5,90\); repeated scalars in a block are last-wins per the sequential if/else consumers (loader-prototypes §M3) — confirm on a fixture in compare mode. (from game-data) - RESOLVED: SAVE_FORMAT tag names — all corrections confirmed by bytes and applied to the Python reader + spec (§10). Residual doc debt:
findings/objects/save-editor-structs.mdandverify/design-rules/SHIP_DESIGN_RULES.mdstill quote R1'sontF/faiDes/nextIdspellings (R1-provenance; annotate rather than rewrite). (from SAVE_FORMAT) - RESOLVED (2026-09-08, lane S):
std::stringlayout and size, once and for all —_Bx@0, _Mysize@0x10, _Myres@0x14, _Alval@0x18, sizeof 0x1c, and there is exactly one instantiation in this binary.struct-recovery.md§0 was right (its §0 prose hadsize/restransposed — fixed);turn-spine.md§1.1 was wrong (already annotated);loader-prototypes.mdand oneaddresses.jsonprototype said 0x18 — fixed. Lane X'sObservedTech0x18 reading was a mis-attribution:+0x24is the string's trailing_Alval, not a data field, proved by three complete enumerations of the element (ObservedTech::Write0x00817cf0, ctor 0x008562a0, copy ctor 0x0079a184) and generalised over the whole exe bytools/strfootprint.py(65 string members, 0 collisions inside the 0x1c span, 51/52 gaps exactly 0x1c). Zero recovered struct tables were wrong —pswdincluded. Standing rule that follows: never size a struct member from the offsets the code touches — this build's STL puts the empty allocator last in bothstring(0x1c) andvector(0x10), and an empty allocator is never loaded or stored, so a touch-scan undercounts by 4 every time. Seeobservedtech-append.md§9. (from re-windows-2000s-howto, observedtech-append) - RESOLVED: formula gaps (all 8) — see formula-gaps; game/sim's low-confidence functions can now be pinned. RESOLVED: std::string =
_Bx@0,_Mysize@0x10,_Myres@0x14,_Alval@0x18, sizeof 0x1c (struct-recovery §0 right; turn-spine §1.1 WRONG — annotated). CORRECTION: the 116-entry table @0x00a19718 is a name-membership list, not the effects table; useg_TechIdNames(196) +OnTechResearched. Open: values of the 6-entry AI-tech bonus table (0x00a17888); producer of ServerPlayer +0x224/+0x228/+0x22c beyond the setup-record copy (0x0077b620). (from tech-effects) - RESOLVED: RNG semantics —
Seedisthiscall(this, uint32)RET 4 (MT19937 init + immediate twist);Twisttakesthisin ECX only; twist is LAZY (if (left==0) Twist()inside the draw),leftlives at+0x9c4, confirming the save blob layout. float mapping, needs a targeted check (not a red flag):NextFloatdecompiles as returningfloat10, but that is simply how x86 float returns look (value inST(0)), so it is weak evidence of extended-precision computation; a singley * 2^-32multiply rounds once either way. B3 should still compare the mapping explicitly (exact constant and whether the draw isy*2^-32,(y>>8)*2^-24, or divided by 2^32-1), and the x87-vs-SSE float-parity policy from the RE how-to is still needed before the x64 standalone.RNG_NextIntsignature still unverified. (own Ghidra pass) - Struct-modelling hazard (found by M2) — an MSVC-2010
std::vectormember is three words, so a naive C translation put the next pointer at+0x18when it is really at+0x14; the hook silently reported the unmodelled word (string bytes"TION") as a real field. Any hand-modelled game struct must pin its size withstatic_assertand account for 3-word vectors. (from M2) - x87 precision-control mode at runtime — 53-bit MSVC default vs 24-bit if D3D9 grabbed the FPU; moves only the last bit (~0.094% of words).
float_from_pc24()models it and the B3 hook logsfpu_cwper call, so the first B3 trace settles it. (from B3) - Some truths are unreachable by compare — B3's draw-divisor bug differed on 0.78% of draws yet flipped 0 decisions in 1e6, so a behavioural compare would have passed while the code was wrong. Static reading of the instruction stream remains necessary; compare validates, it does not discover. (from B3)