sots-re/campaign/research/research-callback-order.md

4.6 KiB

Research callback: fresh instruction-stream findings

Lead: openai/gpt-6-astra, 2026-09-09. Static RE, not live replacement verification. Original binary: dumps/sots.exe, SHA-256 970b7de729956a53094c7eb98aba4270aee98e2fed5daf0d39e290013c90c841 (matches recorded fingerprint). Fresh ReVa reads were cross-checked against local GNU objdump on that executable. No Ghidra edits. Raw/materialized captures: verify/results/research-callback/.

Callback order is observable, not interchangeable

OnTechResearched starts at 0x00891790. The binary establishes this order:

  1. Call RecordObservedTech at 0x008917dc, passing the technology and the player twice; ecx is formed from the server reference at player+8 minus four (when non-null).
  2. Compare completing technology to player+0x294. If matched, optionally call the pending-roll handler at 0x0088df20, then clear player+0x3b4 and player+0x294.
  3. Unless silent, format and post the completion event at 0x008919b5. Its text branches use the research progress-ratio result before applying economic effects.
  4. Execute the technology-specific scalar effects and common completion tail.

Thus the observed-record operation precedes target clearing, and the completion event precedes economic effect writes. A replacement should preserve this order; the earlier conceptual pipeline diagram was not an instruction-order specification.

The two records also use DIFFERENT strings. RecordObservedTech compares/copies the internal technology key from TechDef+4. The completion message fetches TechDef+0x40, with its capacity checked at +0x54 (0x0089180d..0x00891832), before formatting. This is the display-name surface seen in original event prose, not the internal key. Reusing IND_Waldo as event text would be wrong even if the observed-tech name were correct.

W1 IND_Waldo scalar path

MasterTechTree::IsTech is tested with immediate 10001 / 0x2711 at 0x00891a05. The true branch reaches 0x00891a7f: subtract the qword constant at 0x009e2318 from construction modifiers +0x10c/+0x110/+0x114, store each as float32; add the qword constant at 0x009ed188 to output modifier +0x124 and store float32. Raw literals:

  • 0x009e2318: 00 00 00 a0 99 99 b9 3f = double-widened float32 0.1.
  • 0x009ed188: 00 00 00 40 33 33 c3 3f = double-widened float32 0.15.

The common tail invokes bitmask rebuilding, the node-bore updater and species flags. W1's four numeric writes do not justify skipping the common tail or patching later bankruptcy/repair values.

Decompiler hazard: missing post-delete control flow

Fresh ReVa decompilation marks operator_delete at 0x00924faa as non-returning. This truncates real logic: UpdateNodeBoreParams is reported as ending at 0x00818365, immediately after its delete call. Local executable disassembly shows:

  • 0x00818361: call 0x00924faa;
  • 0x00818366: caller stack cleanup;
  • 0x00818369: write zero to player+0x308;
  • 0x00818382: return.

Its positive branch allocates 12 bytes through 0x00924fb6 if the pointer is absent, constructs the object through 0x006e18c0, stores the pointer, and copies three words from selected parameters. It is not a read-only helper. Both allocation/free helpers are import thunks. Fresh PE import inspection pins them to MSVCR100.dll: 0x00924faa jumps through IAT 0x009dd1c8 to ??3@YAXPAX@Z (scalar operator delete); 0x00924fb6 jumps through IAT 0x009dd1d0 to ??2@YAPAXI@Z (scalar operator new). The import descriptor's first thunk is RVA 0x005dd15c; entries 27 and 29 resolve those slots. See pe-headers-and-imports.txt and allocation-import-thunks.txt. This provides a concrete original-runtime allocation boundary; it does not justify substituting the MinGW runtime allocator for memory the original's destructors will free.

The same false non-return annotation appears after long-string temporary cleanup in the main callback. At 0x008919d1 the binary calls delete, then falls through to effect processing at 0x008919d9; the decompiler labels that branch non-returning. Therefore decompiled absence of a write/return after deletion cannot be evidence that the original lacks it.

Astra decision / next RE action

Continue this read-only RE slice with raw instructions as authority on affected lifetime paths. Do not change the shared database during concurrent analysis. Retain the flawed decompilation as evidence of the limitation. No replacement or allocator-safety claim follows from these facts. Next: verify the analyst's ObservedTech/event ownership recovery against the binary, then issue the smallest concrete implementation handoff. The allocator import family is now pinned statically.