b4 live: 36 calls 0 divergences, no colony RNG draws as predicted; fix 3 hook bugs (stale args, two-base StrategyServer, raw Ghidra offset)
This commit is contained in:
parent
a5ffe187ea
commit
bf163a187f
6 changed files with 461 additions and 77 deletions
215
docs/B4.md
215
docs/B4.md
|
|
@ -1,15 +1,31 @@
|
|||
# B4 — the colony turn and the fleet movement pass, old vs new
|
||||
|
||||
**Status (2026-09-08): code complete, cross-built, staged; every VM step still owed.**
|
||||
VM140 was held by another lane for the whole of this milestone, so nothing was deployed, the
|
||||
game was not stopped or relaunched, and `C:\SOTS\binkw32.dll` / `C:\SOTS\shimdist` were not
|
||||
touched. Everything below is offline work plus what the *binary* says; the run list is at the
|
||||
end. The build lives in its own tree (`/srv/re-lab/build/sots-engine-b4`) and its own dist
|
||||
(`/srv/re-lab/shim/dist-b4`), not the shared ones. Build
|
||||
`b4-final-20260908T0500Z`, exports 66 names identical to `binkw32.dll`. Host suite **30/30**. `tools/clean_room_check.sh` OK.
|
||||
**Result (2026-09-08): verified on the live game. 36 compared, 0 divergences.**
|
||||
|
||||
Ghidra was available this round and was used at the end to write the verified prototypes back
|
||||
into the shared project (`reva-server` stopped for the run and restarted afterwards).
|
||||
| pass | build | records | verdict |
|
||||
|---|---|---|---|
|
||||
| scout (`b4scout`) | `b4-fix1-…0600Z` | 28 colony + 1 movement | `tracecmp.py` exit 0, 0 invalid |
|
||||
| trace (`b4trace`) | `b4-fix1-…0600Z` | 28 colony + 7 `MoveFleet` + 1 pass | exit 0, 0 invalid |
|
||||
| compare (`b4compare`) | `b4-fix2-…0615Z` | 36 calls, **all 36 compared** | **0 diverged, 0 errors**, exit 0 |
|
||||
|
||||
The headline is the scout's, and it is what the milestone was for: **the generator did not move
|
||||
on a single one of the 28 systems** — `left` delta 0 and the `mt[624]` block hash identical
|
||||
before and after, on every record — which is exactly what the static sweep predicted (only
|
||||
`ProcessRebellion` draws, and no colony rebelled this turn). `fpu_cw = 0x127f` (53-bit) on all
|
||||
28, confirming the two earlier lanes rather than re-investigating.
|
||||
|
||||
The one fleet that actually moved reproduced bit for bit: fleet 34 went
|
||||
`(-11.9286022, 4.71900415, 2.31785131) -> (-10.5634995, 5.97172451, 1.56473362)` and our
|
||||
float32 position is identical in all three components; its ship's range went 20 -> 18, i.e.
|
||||
exactly `speed 2 x dt 1.0`. `sim::PlanFleetMovement`'s predicted call schedule matched the
|
||||
observed one exactly (all seven fleets, `dt = 1.0`, in fleet-vector order — no pursuits in this
|
||||
save).
|
||||
|
||||
Three bugs in my own hooks were caught by the runs, all of which would have produced a *clean
|
||||
compare that checked nothing* (see "Three hook bugs the live runs caught"). Host suite 30/30,
|
||||
`tools/clean_room_check.sh` OK. Ghidra was used to write the verified prototypes back into the
|
||||
shared project (`reva-server` stopped for the run and restarted afterwards). The VM is released
|
||||
at the main menu with `hooks=trace` and build `b4-fix2-20260908T0615Z` deployed.
|
||||
|
||||
## What was hooked
|
||||
|
||||
|
|
@ -39,6 +55,37 @@ hooked, because M0's lesson is that a wrong `thiscall` prototype crashes the gam
|
|||
`push ecx; fstp DWORD PTR [esp]`. It returns `AL`, and the caller tests it.
|
||||
* `ProcessFleetMovement` ends in a plain `ret` with `mov esi,ecx` and no stack reads.
|
||||
|
||||
## Three hook bugs the live runs caught
|
||||
|
||||
None would have shown up as a divergence. Each would have made a compare that reported
|
||||
"0 diverged" while checking less than it claimed — the same class of failure the harness audit
|
||||
lane is chasing, found the same way B3 found its own: by looking at the trace rather than at
|
||||
the verdict.
|
||||
|
||||
1. **`describe_args` runs before `regions()`.** The template calls them in that order and both
|
||||
descriptors took their snapshot in `regions()`, so every argument sourced from it was **one
|
||||
call stale** — the first record showed zeros, the rest showed the previous system's state.
|
||||
The `inputs` region was always right (its "before" is captured after `regions()` returns), so
|
||||
the compare would have been sound while the trace's own argument record lied. Fixed: capture
|
||||
in `describe_args`, reuse in `regions()`.
|
||||
2. **The StrategyServer has two bases four bytes apart.** A `ServerSystem`'s `owner` word
|
||||
(+0x10) points at a base four bytes *above* the one the class's own methods receive in ECX
|
||||
(the generator accessor at 0x007437f0 does `owner - 4`). Every `StrategyServer_off_*` in the
|
||||
contract is relative to the raw/owner base, so the colony hook — which starts from
|
||||
`sys->owner` — was right, and the two movement hooks, which applied the same offsets to
|
||||
`this`, were not. `ProcessFleetMovement` read an empty player vector, declared **zero**
|
||||
gate-traffic regions, and reported `players=0 fleets=0`; tracecmp dutifully printed "1 call,
|
||||
0 diverged" for a hook that compared nothing. Fixed with an explicit `raw_server()` rebase.
|
||||
3. **`StrategyServer_off_Fleets` was the Ghidra-base number written down as a raw-base one.**
|
||||
With `0x64` instead of `0x60` the hook enumerated the fleet vector's *spare capacity* rather
|
||||
than its elements: the trace pass showed `ProcessFleetMovement` reporting 1 fleet while
|
||||
`MoveFleet` was called 7 times in the same turn. The gate-traffic totals came out all-zero
|
||||
and matched ours — correct by luck, from a garbage fleet list. Fixed to `0x60`; the compare
|
||||
pass then reported 7 fleets in exactly the order `MoveFleet` was called for them.
|
||||
|
||||
The contract entry for `StrategyServer_off_Players` now spells the two-base hazard out, and the
|
||||
three corrected offsets carry the reason in their prototype text.
|
||||
|
||||
## The declared input boundary — say it out loud
|
||||
|
||||
Both colony and movement targets are mostly *dispatchers*. Being honest about that is what
|
||||
|
|
@ -285,62 +332,104 @@ Every constant was checked bit by bit, because B2 and B3 were both bitten here.
|
|||
of records, but on a large map it is every fleet times up to five passes times the recursion
|
||||
depth. The `b4scout` config exists for exactly that reason: it leaves `MoveFleet` off.
|
||||
|
||||
## What remains (needs the VM)
|
||||
## What the runs actually exercised — and what they did not
|
||||
|
||||
The lane holding VM140 must be finished first; then, in this order:
|
||||
A clean compare over 36 calls is worth exactly as much as the coverage behind it, so here is
|
||||
the coverage, region by region, from the compare log itself.
|
||||
|
||||
1. Deploy `/srv/re-lab/shim/dist-b4` (build `b4-final-20260908T0500Z`): `scp` it to
|
||||
`C:\SOTS\shimdist-b4\` and run `deploy.ps1 -Dist C:\SOTS\shimdist-b4` — **a separate staging
|
||||
directory from the shared `C:\SOTS\shimdist`**, so no other lane's dist is overwritten.
|
||||
2. **Scout pass.** Copy `shim.cfg.b4scout` over `C:\SOTS\shim.cfg`, relaunch, load
|
||||
`ref-turn2.sav`, press End Turn once, pull `C:\SOTS\shim.trace.jsonl` →
|
||||
`b4-scout.jsonl`. `tracecmp.py` must exit 0 with 0 invalid records. `MoveFleet` is off in
|
||||
this config, so the file stays small. **Read off it before going further:**
|
||||
* `fpu_cw` on every record — expect `0x027f`; `0x007f` / `0x003f` means 24-bit x87
|
||||
precision and the float mapping needs the PC24 route (B3's open question).
|
||||
* per `ServerSystem::ProcessTurn` record: `args.rng_left_in` minus `side.rng.after.left`.
|
||||
**Expect 0 on every system.** A non-zero delta names a system whose `ProcessRebellion`
|
||||
fired, and that system's compare record is then expected to diverge on `rng` and only on
|
||||
`rng`.
|
||||
* how many systems report `owned`, `stable`, a non-zero `ibon`/`pbon`, a non-empty
|
||||
`civilians` list and a non-zero `bats2`/`rcex`. That is the coverage table for step 4.
|
||||
* per `ProcessFleetMovement` record: the `fleet_state` list — how many fleets exist, how many
|
||||
have waypoints, and whether any waypoint type is 4 or 5. If none is, gate traffic is
|
||||
exercised in its zero branch only and must be reported that way.
|
||||
3. **Trace pass.** Copy `shim.cfg.b4trace` (adds `MoveFleet`), relaunch, load `ref-turn2.sav`,
|
||||
End Turn → `b4-trace-golden.jsonl`. Check the `MoveFleet` record sequence against
|
||||
`sim::PlanFleetMovement`'s prediction: the fleet ids and `dt` values should appear in the
|
||||
pass order documented above. A mismatch is a finding about the schedule, not about the step.
|
||||
4. **Compare pass.** Copy `shim.cfg.b4compare`, relaunch, load `ref-turn2.sav`, End Turn →
|
||||
`b4-compare.jsonl`. Expected:
|
||||
* `ServerSystem::ProcessTurn` — **0 divergences on every system**, on all twelve regions.
|
||||
The reference save is a turn-2 two-empire game, so `infra`/`ibon`/`pbon` are probably
|
||||
exercised in their no-op branches and `bats2`/`rcex` in their all-zero branch; say which
|
||||
regions actually carried a value rather than implying the rest passed.
|
||||
* `MoveFleet` — 0 divergences on `pos`, `prev_pos` and every `ship[i].range` for a call that
|
||||
did **not** arrive. A call that arrived is expected to match on those too (the snap is a
|
||||
verbatim copy) but the RNG and the undeclared arrival state are the original's; a
|
||||
divergence on `pos` after an arrival is a real finding.
|
||||
* `ProcessFleetMovement` — 0 divergences on every `gate_traffic[i]`.
|
||||
Any other diff is a real finding: report it, do not tune the formula.
|
||||
5. Restore the previous `shim.cfg` (`hooks=trace`) and leave the game at the main menu, as
|
||||
M1/M2/B1/B3 left it.
|
||||
**`ServerSystem::ProcessTurn`, 28 calls.** Only **3 systems are owned** (indices 4 and 15, the
|
||||
two players' home worlds; index 16, an independent/NPC colony owned by player 7 and the only
|
||||
non-home one). The other 25 are unowned.
|
||||
|
||||
**Not done, and worth saying:**
|
||||
| region | carried a value? | what that means |
|
||||
|---|---|---|
|
||||
| `ntdev` | **yes, moved on 3** | the stable-increment path is verified on all three owned colonies |
|
||||
| `rcex` | **yes, moved on 6** | the recon countdown sweep is genuinely exercised, including a nibble reaching zero |
|
||||
| `rcex_mask` | one system held a stale bit with a zero counter | the "skip the sweep when the word is zero" branch is verified |
|
||||
| `infra` | matched on all 28, but never *changed* | owned colonies sit at 1.0 (the apply is a no-op) and unowned ones at 0.0, so the decay is only exercised in its clamp branch |
|
||||
| `ibon` / `pbon` | matched, never changed | both home colonies are at their cap, so `ApplyPopBonus` short-circuits and `ApplyInfraBonus` returns at `Infra >= 1`; the accrual gate fails on `ntdev` |
|
||||
| `tres` / `haltv` | matched at 0 / false on all 28 | zero branch only |
|
||||
| `bats2` / `bats_mask` | zero on all 28 | **the battle countdown is completely untested** |
|
||||
| `rng` | matched on all 28, bit for bit | the real result: no colony drew a word |
|
||||
|
||||
**`MoveFleet`, 7 calls.** Six are fleets with **no waypoints at all** (`wpt_type = -1`), which
|
||||
exercise only the early-out. **One call does real work**: fleet 34, waypoint type 1 (a straight
|
||||
run) to a system, `speed 2 x dt 1.0`, and it matched on `pos`, `prev_pos` and its ship's range.
|
||||
So the step arithmetic is verified for exactly one straight-line move. Not exercised at all:
|
||||
the range clamp against a real limit (the ship had 20 range for a 2-unit move), the stranded
|
||||
case, node-line travel (type 2), a node route (type 3), a gate teleport (type 4), a
|
||||
probabilistic jump (type 5, and with it every RNG draw in the movement path), the multi-waypoint
|
||||
recursion, and an arrival.
|
||||
|
||||
**`ProcessFleetMovement`, 1 call.** All eight `gate_traffic` words matched — but every one of
|
||||
them is **0 before and 0 after**. Three fleets carry non-zero traffic words (10, 12, 12…) yet
|
||||
none has a waypoint, so nothing is summed. **The gate-traffic total is verified in its zero
|
||||
branch only.** The pass schedule was checked separately against `sim::PlanFleetMovement` and
|
||||
matched, but this save has no pursuits, so only the "everything else, dt 1.0" pass is covered.
|
||||
|
||||
## Writes of the original that the declared regions do NOT cover
|
||||
|
||||
Stated explicitly, because a clean compare says nothing about any of it. This is the content of
|
||||
the `coverage` block once `HookPolicy` carries the field (the harness audit lane owns that
|
||||
struct change); it lives as a comment at the top of each descriptor header until then.
|
||||
|
||||
**`ServerSystem::ProcessTurn`** — `state: partial`
|
||||
- **the addiction sweep's morale events**: `ours` computes the `{species, event id, delta}`
|
||||
list, but nothing applies it, so the system's `Morale int[7]` and its **morale-event vector
|
||||
append** are never written or compared. *Risk: high — this is precisely B3's failure mode, a
|
||||
list append outside every declared region.* Not exercised by this save either (no addiction).
|
||||
- the whole plague pass, imperial and civilian population growth, `AdjustResources`, the
|
||||
in-orbit refuel, `ProcessSlaves` and `ProcessRebellion`. *Risk: high; no guard.*
|
||||
- the independent-colony imperial↔civilian population drift (0x007514f0). *Risk: medium.*
|
||||
- `TnsOH`, the build queue's own list, and every ship the queue creates. *Risk: medium.*
|
||||
|
||||
**`MoveFleet`** — `state: partial`
|
||||
- **every arrival handler**: `FleetArrives`, the `SEFleetArrived` **event object and its
|
||||
dispatch**, the next-leg pathability check, and the **insert into the server's in-motion set**
|
||||
at +0x210. *Risk: high — an event post and two `std::set` inserts, none declared.*
|
||||
- the departure block: the script hook at StrategyServer+0x1b4, per-ship action cancellation,
|
||||
and `ServerSystem::FleetDeparts` (which rewrites a system's fleet vector and its per-player
|
||||
presence bitmasks). *Risk: high.*
|
||||
- **the waypoint vector itself** — a completed leg pops its waypoint and a failed probabilistic
|
||||
jump rewrites the whole route. *Risk: high.*
|
||||
- the tanker top-up and the fleet flag word at +0x10c. *Risk: medium.*
|
||||
- the node-line step: our side computes `speed x dt` for every waypoint type, so a **type-2
|
||||
waypoint's step is wrong by construction**. *Risk: high; mitigated only by `wpt_type` being on
|
||||
the record — treat type 2 as not compared.*
|
||||
|
||||
**`ProcessFleetMovement`** — `state: partial`
|
||||
- the entire pass schedule and therefore every `MoveFleet` call it makes. *Mitigation:
|
||||
`sim::PlanFleetMovement` predicts the order and the trace was checked against it.*
|
||||
- the per-fleet destination position at +0xec and the flag-0x2 / flag-0x100 clears. *Medium.*
|
||||
- **`OnFleetArrived`**: it takes the set difference of +0x200 and +0x210, clears both, and
|
||||
dispatches a per-player arrival event. *Risk: high.*
|
||||
|
||||
## Runs (`/srv/re-lab/shim/traces/`)
|
||||
|
||||
| file | mode | build | calls | result |
|
||||
|---|---|---|---|---|
|
||||
| `b4-scout.jsonl` | trace | `b4-fix1-…0600Z` | 29 | exit 0, 0 invalid; **RNG delta 0 on all 28 systems** |
|
||||
| `b4-trace-golden.jsonl` | trace | `b4-fix1-…0600Z` | 36 | exit 0, 0 invalid; schedule matched the prediction |
|
||||
| `b4-compare.jsonl` | compare | `b4-fix2-…0615Z` | 36 | **36 compared, 0 diverged, 0 errors** |
|
||||
| `b4-compare-turn2/3.png` | | | | savings 289,688 -> 532,369, the reference oracle state |
|
||||
|
||||
Workload each time: main menu -> Load Game -> Single Player -> `ref-turn2.sav` -> Launch ->
|
||||
turn 2 (savings 289,688) -> **End Turn** -> turn 3 (savings 532,369).
|
||||
|
||||
## Still owed
|
||||
|
||||
* **Replace mode is not offered** for any of the three hooks, so there is no End-Turn oracle
|
||||
result for this milestone. That is a deliberate consequence of the input boundary, not an
|
||||
omission — the strongest available evidence here is the compare plus the RNG post-state.
|
||||
* **Sub-paths the reference save will not exercise**, and which must therefore be reported as
|
||||
untested rather than passed: plague, rebellion (and with it every RNG draw in a colony turn),
|
||||
slaves, terraforming (the reference colony sits at its ideal), the addiction sweep at any
|
||||
phase, the unowned-system infrastructure decay, a non-home colony's `ntdev` reset, the
|
||||
probabilistic jump, the gate teleport, node-line travel, and a stranded fleet. A save with a
|
||||
plague, a rebelling colony, a Hiver gate network and a Zuul node bore would exercise most of
|
||||
them and is the natural next workload.
|
||||
* The **node-line step** is modelled and unit-tested but not hooked (see "What our side runs"),
|
||||
and under shipped data the stutter ramp is a constant anyway (correction 13).
|
||||
* `ComputeOutputFromRates` was read instruction by instruction and every correction is folded
|
||||
into `game/sim/colony`, but it is **not hooked**: it repairs damaged ships in orbit as a side
|
||||
effect (`0x00751590` with its estimate flag clear), so a compare hook cannot run it on a
|
||||
scratch copy of the system without isolating the ships too. That is its own milestone.
|
||||
result for this milestone. That is a consequence of the input boundary above, not an
|
||||
omission: our side models a slice of each function, and feeding that slice to the game would
|
||||
strand every arriving fleet or skip a colony's whole turn.
|
||||
* **A richer save.** The reference save cannot exercise plague, rebellion (and with it every
|
||||
colony RNG draw), slaves, terraforming, the addiction sweep, the battle countdown, a
|
||||
bonus-absorbing colony, gate traffic, node-line travel, a probabilistic jump, an arrival, or
|
||||
a stranded fleet. A save with a plague, a rebelling colony, a Hiver gate network and a Zuul
|
||||
node bore would cover most of them and is the natural next workload.
|
||||
* **The node-line step is not wired into the hook** — it needs the node graph walked from the
|
||||
live server. Until then a type-2 waypoint is compared with the wrong step.
|
||||
* **`ComputeOutputFromRates` is read but not hooked**: it repairs damaged ships in orbit as a
|
||||
side effect, so it cannot run on a scratch copy of the system. Its own milestone.
|
||||
* The three descriptors are `unstated` to the harness; the text above is ready to drop into the
|
||||
`coverage` field the moment `HookPolicy` carries it.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// GENERATED — do not edit. Facts about Sword of the Stars.exe (GOG 1.8.1).
|
||||
// Source: sots-re ghidra/addresses.json @ 8a6c0e8, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Source: sots-re ghidra/addresses.json @ b878c4c, generated 2026-09-08 by tools/gen_addresses.py
|
||||
// Runtime address = (uintptr_t)GetModuleHandle(NULL) + RVA (the exe is ASLR-relocated).
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
|
@ -295,10 +295,6 @@ constexpr uint32_t ServerSystem_CalcSuitMod = 0x003484d0;
|
|||
constexpr uint32_t ServerSystem_AccrueSystemBonus = 0x0034d4f0;
|
||||
// thiscall int (ServerSystem* this, float frac) [verified]
|
||||
constexpr uint32_t ServerSystem_SystemBonusPopTarget = 0x0034b5a0;
|
||||
// cdecl void (Vector3* posOut, bool* arrivedOut, float nodespeed, float dt, void* lines, Vector3* from, Vector3* to) [unverified]
|
||||
constexpr uint32_t NodeLine_Step = 0x00305510;
|
||||
// cdecl void (vector* segsOut, vector* systems, Vector3* from, Vector3* to) [unverified]
|
||||
constexpr uint32_t NodeLine_BuildStutterSegments = 0x00305280;
|
||||
// data struct { const char* name; int x; }[196] /* TechId = 10000+i */ [verified]
|
||||
constexpr uint32_t g_TechIdNames = 0x005ff9e4;
|
||||
// data const char*[116] [verified]
|
||||
|
|
@ -429,5 +425,235 @@ constexpr uint32_t ServerPlayer_SpeciesOfTranslationTech = 0x0040e410;
|
|||
constexpr uint32_t AITechRow = 0x00290f70;
|
||||
// data int -- number of rows in g_AITechValueTable (6) [verified]
|
||||
constexpr uint32_t g_AITechValueCount = 0x006ea2ec;
|
||||
// offset void* -- the StrategyServer subobject pointer; StrategyServer = *(void**)(player+8) - 4. Its +0x16c is the Mars::RNG* the strategic sim draws from, and its +8 the event manager [verified]
|
||||
constexpr uint32_t ServerPlayer_off_ServerLink = 0x00000008;
|
||||
// offset Mars::RNG* -- the strategic generator (same object TechTree::ProcessResearch is handed) [verified]
|
||||
constexpr uint32_t StrategyServer_off_RNG = 0x0000016c;
|
||||
// fastcall void (ServerPlayer* this) -- odds = ResearchEventOdds(this, this->ResT); draws EXACTLY ONE NextFloat from StrategyServer's generator, unconditionally, then fires 0x00889d60 when odds > roll. Called from OnTechResearched when the completing def is the current research target and the pending-roll byte at +0x3b4 is set. This is the extra RNG draw B3 observed on a completion [verified]
|
||||
constexpr uint32_t ServerPlayer_RollResearchEvent = 0x0048df20;
|
||||
// thiscall float (ServerPlayer* this, TechDef* def) -- 0 when def is null; a plague-family path via 0x00535480, else the AI-rebellion path via AITechRow (odds column) gated on !NPC. Read-only, makes no draw [verified]
|
||||
constexpr uint32_t ServerPlayer_ResearchEventOdds = 0x00420380;
|
||||
// thiscall void (ServerSystem* this) /* NO stack arguments -- plain RET, nothing reads [ebp+8]; Ghidra's decompile shows a spurious second parameter. Body order: unowned Infra decay -> ApplyInfraBonus -> ApplyPopBonus -> independent pop drift -> IsStable/ntdev -> AccrueSystemBonus -> ProcessPlague -> ProcessBuildQueue -> imperial growth -> civilian growth -> AdjustResources(-out[2]) -> TRes=0 -> RefuelInOrbit(1) -> Bats2 tick -> rcex tick -> haltv[0..2]=false -> ProcessSlaves -> ProcessRebellion -> addiction sweep. Consumes no RNG itself; ProcessPlague / civilian growth / ProcessSlaves are draw-free to depth 1, and ProcessRebellion is the only consumer */ [verified]
|
||||
constexpr uint32_t ServerSystem_ProcessTurn = 0x003598e0;
|
||||
// thiscall bool (StrategyServer* this, StarFleet* fleet, float dt) /* RET 8; dt is a 4-byte float pushed with fstp DWORD [esp]; returns AL, true only when the fleet ends exactly on a waypoint destination at the deepest recursion level. range = MinRange(fleet, +0.05f) -- ADDED, not subtracted; out of range with MinRange(0)==0 zeroes the RANGE, not the step; move = min(min(range, step), distance) with no floor at 0; recursion iff fraction < (double)0.9999f with dt' = float32((1-fraction)*dt) */ [verified]
|
||||
constexpr uint32_t StrategyServer_MoveFleet = 0x003d9ee0;
|
||||
// thiscall void (StrategyServer* this) /* no stack args, plain RET. Pursuit schedule, not a departing/in-transit split: classify every fleet whose current waypoint targets a fleet by owner relation (0 = pursuer, else follower); pass1 prey dt 0.5, pass2 pursuers dt 0.5 (an arrival retires the pair), pass3 remaining prey dt 0.5, pass4 everything unscheduled dt 1.0 (an uncaught pursuer gets 0.5), pass5 followers dt 1.0. Then FPdpos, gate traffic, OnFleetArrived, and clears flag 0x100 on every fleet. Makes no RNG draw of its own */ [verified]
|
||||
constexpr uint32_t StrategyServer_ProcessFleetMovement = 0x003da9a0;
|
||||
// thiscall float (StarFleet* this, float bias) /* RET 4; min over ships of ship->Range seeded with FLT_MAX (an empty fleet is unconstrained), then + bias, narrowed to float32. No ship is skipped -- range-exempt tankers still clamp the fleet */ [verified]
|
||||
constexpr uint32_t StarFleet_MinRange = 0x002ff6a0;
|
||||
// thiscall bool (StarFleet* this, uint32 mask) /* (flags & mask) == mask, flags at fleet+0x10c */ [verified]
|
||||
constexpr uint32_t StarFleet_HasAllFlags = 0x002fe1d0;
|
||||
// thiscall void (StarFleet* this, uint32 mask, bool on) /* the only two writers of fleet+0x10c in the image */ [verified]
|
||||
constexpr uint32_t StarFleet_SetFlag = 0x002fe1a0;
|
||||
// thiscall void* (StarFleet* this) /* no args; NULL when the waypoint vector is empty or the front waypoint's target id does not resolve in the entity hash at fleet->galaxy(+0x10)+0x80 */ [verified]
|
||||
constexpr uint32_t StarFleet_ResolveWaypoint = 0x00301390;
|
||||
// cdecl bool (int waypointType) /* 7-entry jump table: true only for 4 and 5; false default */ [verified]
|
||||
constexpr uint32_t IsGateTransitWaypoint = 0x0016e6e0;
|
||||
// cdecl bool (int waypointType) /* 7-entry jump table: true only for 3. NOTE the node-LINE case of the movement switch is type 2, which this does NOT accept */ [verified]
|
||||
constexpr uint32_t IsNodeWaypoint = 0x0016e720;
|
||||
// thiscall void (StrategyServer* this, StarFleet* fleet, void* dest) /* waypoint type 5. v = float32(NextFloat() * player->CstE); arrives iff !(v > player->CstT) (equality arrives); on a miss the fleet is placed at dest + randomUnitVector * v -- scattered AROUND the destination by v, not advanced a fraction along the vector -- which costs a second raw draw. 1 draw on success, 2 on a miss */ [verified]
|
||||
constexpr uint32_t ProbabilisticJump = 0x003b6700;
|
||||
// cdecl void (Vector3* posOut, bool* arrivedOut, float nodespeed, float dt, vector<StarSystem*>* systems, const Vector3* from, const Vector3* to) /* builds the stutter segments, reverses them and pops from the back (so ascending by start); plain nodespeed between spheres, a constant per-segment speed inside one; arrived = (time < dt) || |along - length| < FLT_EPSILON, and on arrival the destination is copied verbatim */ [verified]
|
||||
constexpr uint32_t NodeLine_Step = 0x00305510;
|
||||
// cdecl void (vector<StutterSegment>* out, vector<StarSystem*>* systems, const Vector3* from, const Vector3* to) /* chord parameters scaled to world distance and clamped to [0, length]; a chord with |start-end| <= 0.01f is dropped; std::sort ascending by start; then ONE forward pass over adjacent pairs sets BOTH boundaries of an overlap to end_i + 0.5*(end_i - start_{i+1}) -- the mirror of the midpoint, pushed forward past both chords. Nothing is dropped or clipped back, so a swallowed chord comes out inverted */ [verified]
|
||||
constexpr uint32_t NodeLine_BuildStutterSegments = 0x00305280;
|
||||
// cdecl bool (float* tNear, float* tFar, const Sphere4* s, const Vector3* from, const Vector3* to) /* wraps the quadratic solver 0x008a62c0; rejects a sphere the segment does not reach and reports an open end as -FLT_MAX / +FLT_MAX */ [verified]
|
||||
constexpr uint32_t SegmentSphereIntersect = 0x004a64f0;
|
||||
// cdecl float (const Vector3* p, const Vector3* a, const Vector3* b) /* projection parameter clamped to [0,1]. NOTE 0x008c8eb0 is a varargs formatter, not this */ [verified]
|
||||
constexpr uint32_t DistPointToSegment = 0x004e8eb0;
|
||||
// thiscall int (ServerSystem* this, ServerPlayer* p, int flag) /* RET 8 -- (player, flag), NOT (species, groupType). Species is derived from p->Species and the group type is hard-coded to 0, which makes the cross-species and INDSYS branches dead in this specialisation. Returns the int64 result clamped to [0, INT32_MAX], low dword only */ [verified]
|
||||
constexpr uint32_t ServerSystem_MaxPop = 0x0034ab20;
|
||||
// thiscall int64 (ServerSystem* this, int groupType, int species, ServerPlayer* p, float* suitOverride) /* RET 0x10 */ [verified]
|
||||
constexpr uint32_t ServerSystem_MaxPopGeneric = 0x0034a4a0;
|
||||
// thiscall bool (ServerSystem* this) /* false without an owner, with Infra < 1.0, or with Suit != IdealSuit */ [verified]
|
||||
constexpr uint32_t ServerSystem_IsStable = 0x0034ad90;
|
||||
// thiscall void (ServerSystem* this) /* returns early unless ibon > 0 and Infra < 1; resets ntdev to 0 when the system is not the owner's home system; applied = min(ibon, float32(1 - Infra)); Infra becomes EXACTLY 1.0f when applied == the remainder, else float32(Infra + applied); ibon -= applied */ [verified]
|
||||
constexpr uint32_t ServerSystem_ApplyInfraBonus = 0x00346780;
|
||||
// thiscall void (ServerSystem* this) /* nothing unless pbon > 0; an unowned system drops the whole pool; returns when Pop >= MaxPop; resets ntdev for a non-home system; Pop += min(cap - Pop, pbon); pbon -= same. Pop and pbon are int32 */ [verified]
|
||||
constexpr uint32_t ServerSystem_ApplyPopBonus = 0x0034b510;
|
||||
// thiscall void (ServerSystem* this, float delta) /* no-op once Infra >= 1; clamps to 1 and re-normalises the system's own output rates when it lands there */ [verified]
|
||||
constexpr uint32_t ServerSystem_ApplyInfraDelta = 0x00348270;
|
||||
// thiscall void (ServerSystem* this, float delta) /* clamps at the ideal from whichever side it approached, so suitability never overshoots; re-normalises the rates when it lands exactly on the ideal */ [verified]
|
||||
constexpr uint32_t ServerSystem_ApplySuitDelta = 0x003481c0;
|
||||
// thiscall void (ServerSystem* this, int out[12], float rates[7]) /* RET 8. `out` is 12 dwords of MIXED type: 0 total (truncated), 1 strip-mined resources, 2 resources consumed, 3 money, 7 gross construction, 8 points to the queue, 9 points SPENT on repairs, 10 float32 infra delta, 11 float32 suitability delta. `rates` is copied to the stack first, so the caller's struct is untouched. NOT side-effect free: it repairs damaged ships in orbit via 0x00751590(points, estimateOnly=0) */ [verified]
|
||||
constexpr uint32_t ServerSystem_ComputeOutputFromRates = 0x00351bb0;
|
||||
// cdecl void (float rates[7], ServerSystem* sys, float* pinned) /* `pinned` defaults to &rates[0] (trade) and every call site passes NULL or that. Suppress terraform at the ideal (exact ==) and infra when float32(ibon+Infra) >= 1; zero any channel <= (double)1e-4f; clamp ONLY the pinned channel to [0,1]; sum the other three in float32; an exactly-zero sum seeds them with 1e-4f (honouring the suppressions); each becomes (r/sum)*(1-pinned) */ [verified]
|
||||
constexpr uint32_t ServerSystem_NormaliseOutputRates = 0x00347390;
|
||||
// thiscall void (ServerSystem* this, int* resourcesConsumedOut) /* computes the turn's output vector, writes out[2] (resources consumed) through the pointer, applies out[11] via ApplySuitDelta and out[10] via ApplyInfraDelta, and feeds out[8] to BuildQueue::ProcessTurn. Increments TnsOH while SRoh > 0 && out[1] > 0 */ [verified]
|
||||
constexpr uint32_t ServerSystem_ProcessBuildQueue = 0x00352500;
|
||||
// thiscall int (BuildQueue* this, ServerSystem* sys, int points) /* RET 8; `points` is BY VALUE and the leftover is the RETURN value. An order needing more than what is left absorbs everything and stops the pass; a design that costs money asks this->vft[9](sys, (int64)cost) and a refusal SKIPS that order and continues rather than stopping. Removal is a separate sweep afterwards that unlinks every order with conleft <= 0 */ [verified]
|
||||
constexpr uint32_t BuildQueue_ProcessTurn = 0x00490d50;
|
||||
// thiscall void (ServerSystem* this, int delta, int mode) /* RET 8; ProcessTurn passes -out[2] with mode 0 when the owner has AMine, else mode 3. Mode 3 takes it all from Res; mode 0 splits it proportionally over Res / ARes2 / MRes */ [verified]
|
||||
constexpr uint32_t ServerSystem_AdjustResources = 0x00345f30;
|
||||
// thiscall int64 (ServerSystem* this, int groupType, int species) /* RET 8; 0 without an owner or when haltv[groupType] is set */ [verified]
|
||||
constexpr uint32_t ServerSystem_PopGrowthDelta = 0x00348100;
|
||||
// thiscall float (ServerPlayer* p, int groupType, int species, float suit, float factor) /* the growth curve. base = 1 - clamp01(min(|ideal - clamp(suit,0,20)|, SuitTol) / SuitTol) -- there is NO pop/capacity term anywhere; g = clamp01(pow(base, clamp(POPULATION_GROWTH_EXP, 0.01f, 1000))) then x MOD x PopMod x factor x groupdef[+4], each gated on a strict > 0 and each stored back to a float32 */ [verified]
|
||||
constexpr uint32_t PopGrowthFraction = 0x00136fb0;
|
||||
// thiscall float (ServerSystem* this, int species) /* RET 4; 1.0 when unowned. mod = (bit0 ? 0.8f : 1) - 0.2*bit1 - 0.2*bit2, no clamp; rate = ((|Ideal-Suit| x BYHAZARD + DEATH_RATE) + SRs x BYOUTPUT) x mod, every step narrowed to float32 */ [verified]
|
||||
constexpr uint32_t ServerSystem_SlaveDeathRate = 0x0034b110;
|
||||
// thiscall void (ServerSystem* this) /* the worst plague's rate is ADDED to the death rate; SLAVES_MIN/MAX_DEATHS are each disabled by ANY negative value; deaths are clamped into [0, adjusted slave count]. Consumes no RNG */ [verified]
|
||||
constexpr uint32_t ServerSystem_ProcessSlaves = 0x003537b0;
|
||||
// thiscall void (ServerSystem* this) /* consumes no RNG, at depth 1 */ [verified]
|
||||
constexpr uint32_t ServerSystem_ProcessPlague = 0x00356a90;
|
||||
// thiscall void (ServerSystem* this) /* the ONLY RNG consumer in a colony turn, and its draw count is data-dependent: one RandChance per iteration of a 64-bit rebel counter (0x0074fbe0), a short-circuiting per-species roll loop (0x00753c60), one outcome roll (0x00756350) and one 0.2f continuation roll */ [verified]
|
||||
constexpr uint32_t ServerSystem_ProcessRebellion = 0x003583b0;
|
||||
// thiscall void (ServerSystem* this) /* consumes no RNG, at depth 1 */ [verified]
|
||||
constexpr uint32_t ServerSystem_GrowCivilianPops = 0x00354220;
|
||||
// stdcall bool (RNG* this /*ecx, the object*/, float p) /* p <= 0 -> false and p >= 1 -> true, both WITHOUT a draw; otherwise exactly one NextFloat and `r < p` */ [verified]
|
||||
constexpr uint32_t RNG_Chance = 0x004e6dd0;
|
||||
// thiscall void (ServerPlayer* this, MoraleEvent* ev, ServerSystem* sys) /* the actual apply. 0x00752a10 is only the MoraleEvent constructor; the per-species delta is carried in ev->deltas[species] (int[7] at ev+0x18), not as an argument */ [verified]
|
||||
constexpr uint32_t ServerPlayer_AddMoraleEvent = 0x00439d60;
|
||||
// thiscall MoraleEvent* (MoraleEvent* this) /* 0x50 bytes: {vptr, ints (+0x10 = event id), vptr2 @+0x14, int deltas[7] @+0x18, std::string name @+0x34} */ [verified]
|
||||
constexpr uint32_t MoraleEvent_ctor = 0x00352a10;
|
||||
// offset StrategyServer* owner (the RAW base; the RNG accessor uses owner-4) [verified]
|
||||
constexpr uint32_t ServerSystem_off_Owner = 0x00000010;
|
||||
// offset int Idx [verified]
|
||||
constexpr uint32_t ServerSystem_off_Idx = 0x0000005c;
|
||||
// offset int Size (1..10); the capacity base is Size * 100000000 as an exact int64 product [verified]
|
||||
constexpr uint32_t ServerSystem_off_Size = 0x00000060;
|
||||
// offset float Suit [verified]
|
||||
constexpr uint32_t ServerSystem_off_Suit = 0x00000064;
|
||||
// offset int Res [verified]
|
||||
constexpr uint32_t ServerSystem_off_Res = 0x00000068;
|
||||
// offset int ARes2 [verified]
|
||||
constexpr uint32_t ServerSystem_off_ARes2 = 0x0000006c;
|
||||
// offset int MRes [verified]
|
||||
constexpr uint32_t ServerSystem_off_MRes = 0x00000070;
|
||||
// offset int TRes -- zeroed every turn by ProcessTurn [verified]
|
||||
constexpr uint32_t ServerSystem_off_TRes = 0x00000074;
|
||||
// offset bool haltv[3] -- growth halt per group; cleared every turn by ProcessTurn [verified]
|
||||
constexpr uint32_t ServerSystem_off_haltv = 0x00000078;
|
||||
// offset float OutMod [verified]
|
||||
constexpr uint32_t ServerSystem_off_OutMod = 0x0000007c;
|
||||
// offset int TAcq (turn acquired) [verified]
|
||||
constexpr uint32_t ServerSystem_off_TAcq = 0x00000080;
|
||||
// offset StarSystem::OutputRates (0x1c): float SRt, SRsc, SRtf, SRi, SRoh, SRs; int SRnr [verified]
|
||||
constexpr uint32_t ServerSystem_off_Rates = 0x00000088;
|
||||
// offset float SRoh -- the over-harvest slider (Rates + 0x10) [verified]
|
||||
constexpr uint32_t ServerSystem_off_SRoh = 0x00000098;
|
||||
// offset BuildQueue* [verified]
|
||||
constexpr uint32_t ServerSystem_off_BuildQueue = 0x000000a4;
|
||||
// offset std::string Name (0x1c, MSVC SSO; capacity word at +0xbc) [verified]
|
||||
constexpr uint32_t ServerSystem_off_Name = 0x000000a8;
|
||||
// offset bool Abdn [verified]
|
||||
constexpr uint32_t ServerSystem_off_Abdn = 0x000000c4;
|
||||
// offset int64 Bats2 -- player i's 4-bit battle-recent counter at bits [4i, 4i+4), i < 15 [verified]
|
||||
constexpr uint32_t ServerSystem_off_Bats2 = 0x000000f0;
|
||||
// offset int64 rcex -- the same shape for the explored/recon-recent counter [verified]
|
||||
constexpr uint32_t ServerSystem_off_Rcex = 0x000000f8;
|
||||
// offset ServerPlayer* PID (null = unowned) [verified]
|
||||
constexpr uint32_t ServerSystem_off_PID = 0x00000100;
|
||||
// offset Morale cm (0x20: vptr + int[7]) [verified]
|
||||
constexpr uint32_t ServerSystem_off_Morale = 0x0000011c;
|
||||
// offset std::vector<StarFleet*> [verified]
|
||||
constexpr uint32_t ServerSystem_off_Fleets = 0x0000016c;
|
||||
// offset int Pop (imperial) [verified]
|
||||
constexpr uint32_t ServerSystem_off_Pop = 0x0000018c;
|
||||
// offset float Infra [verified]
|
||||
constexpr uint32_t ServerSystem_off_Infra = 0x00000190;
|
||||
// offset int pbon -- the pending population bonus (int32, not a float) [verified]
|
||||
constexpr uint32_t ServerSystem_off_pbon = 0x00000194;
|
||||
// offset float ibon -- the pending infrastructure bonus [verified]
|
||||
constexpr uint32_t ServerSystem_off_ibon = 0x00000198;
|
||||
// offset Population Pop2 (civilians): {vptr, vector<PopulationGroup> @+4}, 24-byte entries {?, int type @+4, int species @+8, int64 count @+0x10} [verified]
|
||||
constexpr uint32_t ServerSystem_off_Pop2 = 0x000001a0;
|
||||
// offset Population pbon2 -- the civilian analogue of pbon [verified]
|
||||
constexpr uint32_t ServerSystem_off_pbon2 = 0x000001b4;
|
||||
// offset IndependenceInfo* indi (int indsp at +4) [verified]
|
||||
constexpr uint32_t ServerSystem_off_Indi = 0x000001c8;
|
||||
// offset int rbfl -- rebelling-species bitmask [verified]
|
||||
constexpr uint32_t ServerSystem_off_rbfl = 0x000001dc;
|
||||
// offset int adt[7] -- per-species addiction start turn; 0 means never addicted [verified]
|
||||
constexpr uint32_t ServerSystem_off_adt = 0x000001e4;
|
||||
// offset uint32 -- bit i is cleared when player i's Bats2 counter reaches 0 [verified]
|
||||
constexpr uint32_t ServerSystem_off_BatsMask = 0x000002a0;
|
||||
// offset uint32 -- the same for rcex [verified]
|
||||
constexpr uint32_t ServerSystem_off_RcexMask = 0x000002a4;
|
||||
// offset int TnsOH (turns over-harvesting) [verified]
|
||||
constexpr uint32_t ServerSystem_off_TnsOH = 0x000002b8;
|
||||
// offset int ntdev -- turns developing; ++ when IsStable, reset to 0 otherwise AND by either bonus-apply helper on a non-home system. This is the second SYSTEMBONUS_MINTURNS gate, not rbtn [verified]
|
||||
constexpr uint32_t ServerSystem_off_ntdev = 0x000002c4;
|
||||
// offset int rbtn [verified]
|
||||
constexpr uint32_t ServerSystem_off_rbtn = 0x000002cc;
|
||||
// offset int ModCount (the turn counter), relative to the RAW server base a ServerSystem's +0x10 points at [verified]
|
||||
constexpr uint32_t StrategyServer_off_ModCount = 0x00000008;
|
||||
// offset std::vector<ServerPlayer*> (begin @+0x50, end @+0x54); numPlayers = (end-begin)>>2 /* TWO BASES: every StrategyServer_off_* here is relative to the RAW base a ServerSystem's owner word (+0x10) points at. The class's own methods receive a base FOUR BYTES LOWER in ECX (0x007437f0 does owner-4), so a hook on MoveFleet or ProcessFleetMovement must add 4 to `this` before applying these */ [verified]
|
||||
constexpr uint32_t StrategyServer_off_Players = 0x00000050;
|
||||
// offset std::vector<StarFleet*> (begin @+0x60, end @+0x64) -- ALL fleets of ALL players, one flat global list. B4 live: the earlier 0x64 was the Ghidra-base number transcribed as a raw-base one; it made ProcessFleetMovement enumerate the vector's spare capacity instead of its elements [verified]
|
||||
constexpr uint32_t StrategyServer_off_Fleets = 0x00000060;
|
||||
// offset 16-bucket hash of entity id -> object, hashed by (key & 0xF); 0x008b9240 is the lookup [verified]
|
||||
constexpr uint32_t StrategyServer_off_EntityHash = 0x00000080;
|
||||
// offset Mars::RNG* relative to the RAW base (== the -4-adjusted base's +0x16c) [verified]
|
||||
constexpr uint32_t StrategyServer_off_RNGPtr = 0x00000168;
|
||||
// offset std::set<pair<FleetId,LocationId>> cleared at the head of ProcessFleetMovement [verified]
|
||||
constexpr uint32_t StrategyServer_off_ArrivedSet = 0x00000200;
|
||||
// offset std::set<pair<FleetId,LocationId>> that MoveFleet fills for fleets still in motion; OnFleetArrived takes the set difference [verified]
|
||||
constexpr uint32_t StrategyServer_off_InMotionSet = 0x00000210;
|
||||
// offset int id (also the entity-hash key) [verified]
|
||||
constexpr uint32_t StarFleet_off_Id = 0x00000004;
|
||||
// offset the object whose +0x80 holds the entity hash the waypoint target is resolved in [verified]
|
||||
constexpr uint32_t StarFleet_off_Galaxy = 0x00000010;
|
||||
// offset Vector3 Pos [verified]
|
||||
constexpr uint32_t StarFleet_off_Pos = 0x00000018;
|
||||
// offset Vector3 PrvPos -- set to the ENTRY position when the fleet moved [verified]
|
||||
constexpr uint32_t StarFleet_off_PrvPos = 0x0000004c;
|
||||
// offset ServerPlayer* owner [verified]
|
||||
constexpr uint32_t StarFleet_off_PID = 0x00000058;
|
||||
// offset Location*; kind at Location+0x14 (0 system, 1 fleet, 2 point) [verified]
|
||||
constexpr uint32_t StarFleet_off_Location = 0x000000a0;
|
||||
// offset std::vector<StarShip*> (begin @+0xa4, end @+0xa8) [verified]
|
||||
constexpr uint32_t StarFleet_off_Ships = 0x000000a4;
|
||||
// offset int16 -- SIGNED, summed into the owner's gate traffic [verified]
|
||||
constexpr uint32_t StarFleet_off_GateTraffic = 0x000000c0;
|
||||
// offset std::vector<Waypoint> _Myfirst (proxy @+0xc4, last @+0xcc, end @+0xd0) [verified]
|
||||
constexpr uint32_t StarFleet_off_Waypoints = 0x000000c8;
|
||||
// offset float FPsp2 -- the fleet's strategic speed [verified]
|
||||
constexpr uint32_t StarFleet_off_Speed = 0x000000d8;
|
||||
// offset Vector3 -- set after every pass to the current waypoint target's position. The notes called this FPogn2; by the FlightPlan layout it is FPdpos [verified]
|
||||
constexpr uint32_t StarFleet_off_DestPos = 0x000000ec;
|
||||
// offset uint32; bit 0x100 = held this turn (cleared for every fleet at the end of ProcessFleetMovement), bit 0x2 cleared in the destination pass, bit 0x1 set by MoveFleet when the fleet moved [verified]
|
||||
constexpr uint32_t StarFleet_off_Flags = 0x0000010c;
|
||||
// offset sizeof(Waypoint) -- pinned by the divide-by-28 reciprocal multiply in the iterator arithmetic [verified]
|
||||
constexpr uint32_t Waypoint_stride = 0x0000001c;
|
||||
// offset int -- the destination entity id [verified]
|
||||
constexpr uint32_t Waypoint_off_Target = 0x00000004;
|
||||
// offset int -- 2 node line, 3 node route, 4 gate teleport, 5 probabilistic jump, otherwise a straight run [verified]
|
||||
constexpr uint32_t Waypoint_off_Type = 0x00000008;
|
||||
// offset uint64 flag pair; bit 0x1000 exempts the ship from movement fuel and marks it a tanker [verified]
|
||||
constexpr uint32_t StarShip_off_Flags = 0x00000018;
|
||||
// offset float Range -- remaining strategic range [verified]
|
||||
constexpr uint32_t StarShip_off_Range = 0x00000020;
|
||||
// offset float -- the cap the refuel helper clamps Range to [verified]
|
||||
constexpr uint32_t StarShip_off_MaxRange = 0x00000078;
|
||||
// offset ServerSystem* -- the bonus-apply helpers skip the ntdev reset for this system [verified]
|
||||
constexpr uint32_t ServerPlayer_off_HomeSystem = 0x0000002c;
|
||||
// offset int GTraf -- assigned (not accumulated) once per turn [verified]
|
||||
constexpr uint32_t ServerPlayer_off_GateTraffic = 0x0000014c;
|
||||
// offset float CstE -- the probabilistic jump's efficiency [verified]
|
||||
constexpr uint32_t ServerPlayer_off_CstE = 0x00000154;
|
||||
// offset float CstT -- the probabilistic jump's threshold [verified]
|
||||
constexpr uint32_t ServerPlayer_off_CstT = 0x00000158;
|
||||
// data int** -- the GlobalConst pointer slot; the storage word is *slot [verified]
|
||||
constexpr uint32_t g_ptr_SYSTEMBONUS_MINTURNS = 0x006eca10;
|
||||
// data float** -- pointer slot [verified]
|
||||
constexpr uint32_t g_ptr_SYSTEMBONUS_POPBONUS = 0x006eca18;
|
||||
// data float** -- pointer slot [verified]
|
||||
constexpr uint32_t g_ptr_SYSTEMBONUS_INFRABONUS = 0x006eca20;
|
||||
// data float** -- pointer slot [verified]
|
||||
constexpr uint32_t g_ptr_SYSTEMBONUS_POPBONUS_INC = 0x006eca38;
|
||||
// data float** -- pointer slot [verified]
|
||||
constexpr uint32_t g_ptr_SYSTEMBONUS_INFRABONUS_INC = 0x006eca40;
|
||||
// data int** -- pointer slot (image default 10) [verified]
|
||||
constexpr uint32_t g_ptr_ADDICTION_PHASE2_START = 0x006eca58;
|
||||
// data int** -- pointer slot (image default 15) [verified]
|
||||
constexpr uint32_t g_ptr_ADDICTION_PHASE3_START = 0x006eca60;
|
||||
// data float -- 0.05f, a hard-coded literal in .data, NOT a GlobalConst; both ServerSystem::ProcessTurn calls to the independent pop-drift helper pass it [verified]
|
||||
constexpr uint32_t g_IndependentPopDriftRate = 0x006eca80;
|
||||
// data float** -- pointer slot; storage 0x00b212cc (shipped value 2) [verified]
|
||||
constexpr uint32_t g_ptr_STUTTER_SYSTEM_INFLUENCE_RADIUS = 0x006ebc44;
|
||||
// data float** -- pointer slot; storage 0x00b212d0 (shipped value 0.33) [verified]
|
||||
constexpr uint32_t g_ptr_STUTTER_MIN_SPEED = 0x006ebc48;
|
||||
// data float** -- pointer slot; storage 0x00b212d4 (shipped value 0.33). NOTE min == max in the shipped data, so the stutter ramp collapses to a constant 0.33x inside any influence sphere [verified]
|
||||
constexpr uint32_t g_ptr_STUTTER_MAX_SPEED = 0x006ebc4c;
|
||||
|
||||
} // namespace sots::addr
|
||||
|
|
|
|||
|
|
@ -323,6 +323,9 @@ void init_colony_turn(std::uintptr_t exe_base, void (*log_line)(const char* line
|
|||
}
|
||||
|
||||
void ServerSystemProcessTurnHook::describe_args(std::vector<Tv>& out, void* self) {
|
||||
// The template calls describe_args BEFORE regions, so the snapshot is taken here and
|
||||
// regions reuses it. Capturing in regions() left every argument one call stale.
|
||||
capture(self);
|
||||
out.push_back(tv::ptr(self).named("system"));
|
||||
// The snapshot is the whole input record; it doubles as the `inputs` region below, but the
|
||||
// harness never compares arguments, so the interesting context lives here.
|
||||
|
|
@ -333,7 +336,6 @@ void ServerSystemProcessTurnHook::describe_args(std::vector<Tv>& out, void* self
|
|||
}
|
||||
|
||||
void ServerSystemProcessTurnHook::regions(std::vector<trace::Region>& out, void* self) {
|
||||
capture(self);
|
||||
if (!self) return;
|
||||
|
||||
// Only the words `sim::ProcessColonyTurn` models are declared. Everything the callees
|
||||
|
|
|
|||
|
|
@ -17,6 +17,24 @@
|
|||
// ProcessRebellion (ProcessPlague, the civilian growth pass and ProcessSlaves were each swept
|
||||
// to call depth one and are draw-free), so on a system with no rebellion the post-call state
|
||||
// must be **identical** -- and any movement of it names the system whose rebellion fired.
|
||||
// COVERAGE STATEMENT (for meta.hooks[H].coverage once HookPolicy carries the field; the
|
||||
// harness audit lane owns that struct change, so this lives as text until it lands).
|
||||
// state: "partial"
|
||||
// unmodelled:
|
||||
// - what: "the whole plague pass (ProcessPlague), imperial and civilian population growth,
|
||||
// the resource debit (AdjustResources), the in-orbit refuel, ProcessSlaves and
|
||||
// ProcessRebellion"
|
||||
// risk: high mitigation: "none - those fields are not declared regions at all"
|
||||
// - what: "the addiction sweep's morale events: ours computes the {species, event id,
|
||||
// delta} list but nothing applies it, so the system's Morale int[7] and its
|
||||
// morale-event vector are never written or compared"
|
||||
// risk: high why: "this is exactly B3's failure mode - a list append outside every
|
||||
// declared region" mitigation: "guard:morale (not yet declared)"
|
||||
// - what: "the independent-colony imperial<->civilian pop drift (0x007514f0), called
|
||||
// between the bonus pools and the stability check"
|
||||
// risk: medium mitigation: "none"
|
||||
// - what: "TnsOH, the build queue's own list, and every ship the queue creates"
|
||||
// risk: medium mitigation: "none"
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,18 @@ constexpr std::size_t kRngSize = A::RNG_size;
|
|||
constexpr int kMtWords = mars::rng::MT19937::N;
|
||||
constexpr std::size_t kMaxPlayers = 64;
|
||||
|
||||
// The StrategyServer has two bases in play. A ServerSystem's `owner` word (+0x10) points at a
|
||||
// base four bytes ABOVE the one the class's own methods receive in ECX -- the accessor at
|
||||
// 0x007437f0 does `owner - 4` before reading the generator. Every `StrategyServer_off_*` in the
|
||||
// address contract is relative to the **raw** (owner) base, so a hook whose `this` is the
|
||||
// method base has to add four before applying them. Getting this wrong silently reads a
|
||||
// neighbouring field: the first scout run declared zero gate-traffic regions because the
|
||||
// player vector came back empty.
|
||||
constexpr std::size_t kServerRawDelta = 4;
|
||||
void* raw_server(void* self) {
|
||||
return self ? static_cast<char*>(self) + kServerRawDelta : nullptr;
|
||||
}
|
||||
|
||||
using ResolveWaypointFn = void*(SHIM_THISCALL*)(void* fleet);
|
||||
using RelationFn = int(SHIM_THISCALL*)(void* player, void* other);
|
||||
|
||||
|
|
@ -89,7 +101,8 @@ std::uint32_t fpu_control_word() {
|
|||
#endif
|
||||
}
|
||||
|
||||
void* rng_of(void* server) {
|
||||
void* rng_of(void* self) {
|
||||
void* server = raw_server(self);
|
||||
if (!readable(server, A::StrategyServer_off_RNGPtr + 4)) return nullptr;
|
||||
void* r = ptr_at(server, A::StrategyServer_off_RNGPtr);
|
||||
return readable(r, kRngSize) ? r : nullptr;
|
||||
|
|
@ -119,8 +132,9 @@ std::vector<void*> fleet_ships(const void* fleet) {
|
|||
return ships;
|
||||
}
|
||||
|
||||
std::vector<void*> server_players(void* server) {
|
||||
std::vector<void*> server_players(void* self) {
|
||||
std::vector<void*> out;
|
||||
void* server = raw_server(self);
|
||||
if (!readable(server, A::StrategyServer_off_Players + 8)) return out;
|
||||
void** begin = static_cast<void**>(ptr_at(server, A::StrategyServer_off_Players));
|
||||
void** end = static_cast<void**>(ptr_at(server, A::StrategyServer_off_Players + 4));
|
||||
|
|
@ -131,8 +145,9 @@ std::vector<void*> server_players(void* server) {
|
|||
return out;
|
||||
}
|
||||
|
||||
std::vector<void*> server_fleets(void* server) {
|
||||
std::vector<void*> server_fleets(void* self) {
|
||||
std::vector<void*> out;
|
||||
void* server = raw_server(self);
|
||||
if (!readable(server, A::StrategyServer_off_Fleets + 8)) return out;
|
||||
void** begin = static_cast<void**>(ptr_at(server, A::StrategyServer_off_Fleets));
|
||||
void** end = static_cast<void**>(ptr_at(server, A::StrategyServer_off_Fleets + 4));
|
||||
|
|
@ -306,6 +321,9 @@ void init_fleet_movement(std::uintptr_t exe_base, void (*log_line)(const char* l
|
|||
|
||||
void StrategyServerMoveFleetHook::describe_args(std::vector<Tv>& out, void* self, void* fleet,
|
||||
float dt) {
|
||||
// The template calls describe_args BEFORE regions, so the snapshot is taken here; regions
|
||||
// then reuses it. Capturing in regions() left every argument one call stale.
|
||||
capture_move(self, fleet, dt);
|
||||
out.push_back(tv::ptr(self).named("server"));
|
||||
out.push_back(tv::ptr(fleet).named("fleet"));
|
||||
out.push_back(tv::f32(dt).named("dt"));
|
||||
|
|
@ -323,7 +341,8 @@ Tv StrategyServerMoveFleetHook::describe_ret(bool r) { return tv::boolean(r); }
|
|||
|
||||
void StrategyServerMoveFleetHook::regions(std::vector<trace::Region>& out, void* self,
|
||||
void* fleet, float dt) {
|
||||
capture_move(self, fleet, dt);
|
||||
(void)self; // describe_args already captured everything this needs
|
||||
(void)dt;
|
||||
if (!readable(fleet, A::StarFleet_off_Flags + 4)) return;
|
||||
|
||||
trace::Region pos;
|
||||
|
|
@ -441,6 +460,8 @@ bool StrategyServerMoveFleetHook::ours(void* self, void* fleet, float dt) {
|
|||
// ---- ProcessFleetMovement --------------------------------------------------------------------
|
||||
|
||||
void StrategyServerProcessFleetMovementHook::describe_args(std::vector<Tv>& out, void* self) {
|
||||
// Same ordering rule as MoveFleet: capture here, before regions runs.
|
||||
capture_pfm(self);
|
||||
out.push_back(tv::ptr(self).named("server"));
|
||||
out.push_back(tv::i32(g_pfm.playerCount).named("players"));
|
||||
out.push_back(tv::i32(static_cast<std::int32_t>(g_pfm.fleets.size())).named("fleets"));
|
||||
|
|
@ -458,10 +479,9 @@ void StrategyServerProcessFleetMovementHook::describe_args(std::vector<Tv>& out,
|
|||
|
||||
void StrategyServerProcessFleetMovementHook::regions(std::vector<trace::Region>& out,
|
||||
void* self) {
|
||||
// Captured BEFORE the original runs, so the argument record shows the pre-move fleet
|
||||
// state; `ours` re-captures afterwards, because that is when the original sums the
|
||||
// traffic.
|
||||
capture_pfm(self);
|
||||
// describe_args already captured the pre-move fleet state that the argument record shows;
|
||||
// `ours` re-captures afterwards, because that is when the original sums the traffic.
|
||||
(void)self;
|
||||
g_pfm.names.clear();
|
||||
g_pfm.names.reserve(g_pfm.players.size());
|
||||
for (std::size_t i = 0; i < g_pfm.players.size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,35 @@
|
|||
// input boundary: everything else. The pass schedule is recorded in the arguments (ours
|
||||
// predicts the call order so a trace can be checked against it) but is not
|
||||
// itself compared, because reproducing it would mean running MoveFleet.
|
||||
// COVERAGE STATEMENT (see the note in colony_turn.h about where this will live).
|
||||
//
|
||||
// MoveFleet -- state: "partial"
|
||||
// - what: "every arrival handler: FleetArrives, the SEFleetArrived event object and its
|
||||
// dispatch, the next-leg pathability check, and the insert into the server's
|
||||
// in-motion set at +0x214"
|
||||
// risk: high why: "an event post and two std::set inserts, none of them declared"
|
||||
// - what: "the departure block: the script hook at StrategyServer+0x1b4, per-ship action
|
||||
// cancellation, and ServerSystem::FleetDeparts (which rewrites a system's fleet
|
||||
// vector and its per-player presence bitmasks)"
|
||||
// risk: high mitigation: "none"
|
||||
// - what: "the waypoint vector itself - a leg that completes pops its waypoint, and a failed
|
||||
// probabilistic jump rewrites the whole route"
|
||||
// risk: high mitigation: "none"
|
||||
// - what: "the tanker top-up (flag 0x1000 ships are refuelled to full each step) and the
|
||||
// fleet flag word at +0x10c (bit 1 set when the fleet moved)"
|
||||
// risk: medium mitigation: "none"
|
||||
// - what: "the node-line step: our side computes speed x dt for every waypoint type, so a
|
||||
// type-2 waypoint's step is wrong by construction"
|
||||
// risk: high mitigation: "the record carries wpt_type; treat type 2 as not compared"
|
||||
//
|
||||
// ProcessFleetMovement -- state: "partial"
|
||||
// - what: "the entire pass schedule and therefore every MoveFleet call it makes"
|
||||
// risk: high mitigation: "sim::PlanFleetMovement predicts the order; check the trace"
|
||||
// - what: "the per-fleet destination position at +0xec and the flag-0x2 / flag-0x100 clears"
|
||||
// risk: medium mitigation: "none"
|
||||
// - what: "OnFleetArrived: it takes the set difference of +0x204 and +0x214, clears both,
|
||||
// and dispatches a per-player arrival event"
|
||||
// risk: high mitigation: "none"
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue