`ComputeBudget`'s savings-interest term is now compared against the running game at
a treasury the corpus actually contains. Three runs on VM146 from turn1-state.sav:
A (widened floats, as shipped) 3,895 calls, 0 diverged, 0 undeclared writes
B (exact decimals, the control) 2,718 calls, 1,359 diverged
The game fills savingsInterest with 499 at a treasury of 50,000, and with 380 at
38,100 -- the exact decimals pay 500 and 381. Every divergence in B lands on a
treasury that is a multiple of 100 and no other state diverges at all, which is
exactly the arithmetic. G3's rule-23 reading is now measured, not inferred, and the
one-money error is shown to propagate into `available` and `researchMoney` too.
The control also settles why the earlier 4,437-call green run was green: slot 5 IS
diffed and the harness CAN see it, so that run simply presented no boundary state.
Coverage is therefore reported as distinct states, not calls: 5 distinct treasuries,
2 of them on the boundary.
Two further rule-23 constants found in the same routine by an operand-width sweep,
corrected, and honestly marked UNVERIFIED because no reference turn can see them:
- the research-yield factor is a widened 0.85f while its two neighbours in the
same product are exact doubles. Boundary: research money a multiple of 40,000;
the run presented 9 distinct values and none is.
- the three research modifiers are summed in single precision, not double.
Boundary: two of the three non-zero; the corpus has shrm = TRM = 0.
Both are pinned by boundary cases in test_economy.cpp that fail with the decimals.
Also verified live, in the same run:
- T31's difficulty-column recovery. The live ServerPlayer+0xf9 / NPC flags on all
eight players are exactly what lane PL's save-only inversion claims, including
the awkward system-owning player that is still ambiguous because it is an NPC.
- BANKRUPTCY_PROTECTION_LIMIT_FACTOR reads 3.29999995 = (float)3.3. Its file image
is zero because the loader fills it at run time, so lane PL-3 had to assume the
value; it is now measured and the assumption was right.
Falsified, and recorded as such: the difficulty-mods record does NOT sit inline at
ServerPlayer+0x36c -- that field is a heap pointer on all eight players. The row IS
reachable from a ServerPlayer (which corrects the hook's standing coverage note),
but the fitted {3.0,1.5}/{1.0,1.0} pair remains unverified. The hook logs the
pointer and does not follow it.
The `verified` column stays 0, deliberately. Every phase this compare touches is
Partial for reasons upstream of it, and promoting one because part of it was checked
is the drift app_test_catalog exists to catch. What moved is models; see
docs/L5-live-verification.md for each one with its coverage.
Gates run separately: clean-room OK, host ctest 54/54, CT111 shim cross-build exit 0.
411 lines
28 KiB
C++
411 lines
28 KiB
C++
#include "app/phase_catalog.h"
|
|
|
|
namespace sots::app {
|
|
|
|
const char* DriverName(Driver d) {
|
|
switch (d) {
|
|
case Driver::Host: return "host turn sequence (outside the two turn drivers)";
|
|
case Driver::Strategic: return "StrategyServer::ProcessTurn";
|
|
case Driver::Player: return "ServerPlayer::ProcessTurn";
|
|
case Driver::Tail: return "StrategyServer::OnAllCombatDone_Tail";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
const char* StatusName(PhaseStatus s) {
|
|
switch (s) {
|
|
case PhaseStatus::Verified: return "verified";
|
|
case PhaseStatus::Implemented: return "implemented";
|
|
case PhaseStatus::Partial: return "partial";
|
|
case PhaseStatus::Blocked: return "blocked";
|
|
case PhaseStatus::Stub: return "stub";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
char StatusGlyph(PhaseStatus s) {
|
|
switch (s) {
|
|
case PhaseStatus::Verified: return 'V';
|
|
case PhaseStatus::Implemented: return 'I';
|
|
case PhaseStatus::Partial: return 'P';
|
|
case PhaseStatus::Blocked: return 'B';
|
|
case PhaseStatus::Stub: return '.';
|
|
}
|
|
return '?';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// StrategyServer::ProcessTurn -- 32 phases, 0..31
|
|
// ---------------------------------------------------------------------------------------
|
|
namespace {
|
|
|
|
// The host steps around the drivers. Two turn counters live here, not in either driver.
|
|
constexpr PhaseDesc kHost[] = {
|
|
{Driver::Host, 0, "H00", "BeginProcessTurn", PhaseStatus::Implemented,
|
|
"advances the frame counter, which is the turn number the whole game displays; runs "
|
|
"before the spine and is where the 'Begin processing turn N' line comes from"},
|
|
{Driver::Host, 1, "H03", "ScriptHookTurnBegin", PhaseStatus::Partial,
|
|
"the turn's FIRST script-object event delivery, sent from inside BeginProcessTurn right "
|
|
"after the frame counter. Two of the twelve script objects our saves carry react to it. "
|
|
"The refugees' one-shot latch IS modelled and committed; the design instantiation behind "
|
|
"the same latch is not, because nothing here allocates handles. The swarm queen's hive "
|
|
"registration, prune and per-turn slip ARE modelled -- which system each hive sits on and "
|
|
"that it has no queen are exact -- but the hive's target turn is drawn from the strategic "
|
|
"generator here, outside both turn drivers, and that draw is not placed. See "
|
|
"docs/SV-script-objects.md"},
|
|
{Driver::Host, 2, "H02", "StampTreatyTurns", PhaseStatus::Implemented,
|
|
"the diplomacy ledger's 'this treaty was last in force on turn N' stamp, over every "
|
|
"ORDERED pair of players that holds one, creating the entry on demand. Runs in the "
|
|
"command-application step -- after the frame counter has advanced and before either "
|
|
"turn driver -- so it stamps the NEW turn. It is the only writer of these fields on a "
|
|
"turn with no combat and no diplomatic command"},
|
|
{Driver::Host, 3, "H01", "SaveWriterInvariants", PhaseStatus::Implemented,
|
|
"the summary's turn number is the simulation's frame counter -- an identity that holds "
|
|
"across every save in the corpus and belongs to the writer, not to a turn phase"},
|
|
};
|
|
|
|
constexpr PhaseDesc kStrategic[] = {
|
|
{Driver::Strategic, 0, "S00", "SnapshotPreviousTurn", PhaseStatus::Partial,
|
|
"bumps the modification counter and stamps PvSav from Sav on every live player -- the "
|
|
"treasury's previous-turn shadow, taken before any phase of the turn can move it, which "
|
|
"is what makes the tail's Sav-PvSav the turn's own net. Measured on the reference pairs: "
|
|
"0 closed on turn1->turn2 (the snapshot is a no-op there, every treasury is untouched) "
|
|
"and 2 closed on turn2->turn3, 0 regressed on both. It is short by exactly 11,900 on the "
|
|
"one AI empire, whose queued build order is deducted before this phase and exists nowhere "
|
|
"in the input file (Rung B). The REST of the shadow words are still not modelled: they "
|
|
"are not all identified on the wire"},
|
|
{Driver::Strategic, 1, "S01", "SystemPrePassMoraleAndAbandon", PhaseStatus::Stub,
|
|
"per-system morale event + the abandon/chaos check below the minimum chaos population"},
|
|
{Driver::Strategic, 2, "S02", "TradeManagerTurn", PhaseStatus::Stub,
|
|
"ServerTradeManager::ProcessTurn -- feeds the trade slot of the budget"},
|
|
{Driver::Strategic, 3, "S03", "RegisterTradeSystems", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 4, "S04", "RebuildAllianceMasks", PhaseStatus::Implemented,
|
|
"per-player shared-vision / alliance mask, rebuilt from scratch each turn: the player's "
|
|
"own bit -- its POSITION IN THE PLAYER VECTOR, not its index field -- OR the alliance's "
|
|
"member mask when the player carries an alliance id. The word is not a leaf of its own; "
|
|
"it reaches the wire through the tail's turn-record archive, and it agrees with the "
|
|
"archived bytes on 72 of 80 player-records with the other 8 predicted"},
|
|
{Driver::Strategic, 5, "S05", "BuildShipActionTypeSets", PhaseStatus::Stub,
|
|
"builds the two action-type id sets the dispatcher runs over"},
|
|
{Driver::Strategic, 6, "S06", "ShipActionsExceptType2", PhaseStatus::Stub,
|
|
"the ship-action dispatcher over every action type but type 2 (colonise, build, "
|
|
"terraform, mine, scrap)"},
|
|
{Driver::Strategic, 7, "S07", "NodeSpaceTravel", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 8, "S08", "FleetMovement", PhaseStatus::Stub,
|
|
"game::sim movement primitives exist and are mechanism-verified, but the fleet/waypoint "
|
|
"adapter over the save's flight plans is not written"},
|
|
{Driver::Strategic, 9, "S09", "ShipActionsType2", PhaseStatus::Stub,
|
|
"the action type that needs the fleet to have arrived first"},
|
|
{Driver::Strategic, 10, "S10", "ShipUpkeep", PhaseStatus::Stub,
|
|
"upkeep of population carried aboard colony/slaver hulls in transit"},
|
|
{Driver::Strategic, 11, "S11", "SystemTurn", PhaseStatus::Partial,
|
|
"runs game::sim ProcessColonyTurn per system and commits the parts that need neither the "
|
|
"tuning table nor a carrying capacity. CIVILIAN GROWTH now runs and commits: the whole "
|
|
"system's civilian delta is clamped to POPTYPE[1]+0x08 = 20,000,000, an int64 literal in "
|
|
"the executable, and on both reference pairs that clamp -- not the growth curve and not "
|
|
"any capacity -- is what decides the value (the uncapped delta is 7.5x it and the "
|
|
"capacity headroom 25x it). The one input that is genuinely off the wire, the per-species "
|
|
"civilian capacity factor, is handled by running the pass twice, once with the modelled "
|
|
"capacity and once with the system's own wire-known dcs limit, and committing only when "
|
|
"the two agree. Plague, resources, slaves and rebellion remain the model's input "
|
|
"boundary, as does IMPERIAL growth -- it is a no-op on this corpus (the homeworld sits "
|
|
"exactly at its cap) and committing it would need a capacity the corpus cannot bound "
|
|
"from above. The BUILD QUEUE is "
|
|
"modelled and runs (it is the only writer of the per-class built counter in the whole "
|
|
"image) but has no points to spend: they come from the per-system output term. It is "
|
|
"NOT what the missing destroyer waits on -- no build order exists anywhere in either "
|
|
"reference save, so that order is created inside the turn by the AI"},
|
|
{Driver::Strategic, 12, "S12", "TradeSliderFinalisation", PhaseStatus::Stub,
|
|
"re-normalises the per-system output rates"},
|
|
{Driver::Strategic, 13, "S13", "PlayerTurn", PhaseStatus::Partial,
|
|
"runs the 12-phase player driver once per player, in save order"},
|
|
{Driver::Strategic, 14, "S14", "ProcessMissions", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 15, "S15", "ProcessStations", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 16, "S16", "ProcessDefenceSats", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 17, "S17", "ShipStatCacheRefresh", PhaseStatus::Stub,
|
|
"re-syncs cached per-ship stat words from the design record"},
|
|
{Driver::Strategic, 18, "S18", "ShipActionsForceValidate", PhaseStatus::Stub,
|
|
"the dispatcher a third time over all action types with the force flag: validate and "
|
|
"cancel whatever is left"},
|
|
{Driver::Strategic, 19, "S19", "ProcessAid", PhaseStatus::Stub,
|
|
"writes savings and research points of OTHER players; an input to the budget"},
|
|
{Driver::Strategic, 20, "S20", "ProcessSpecialProjectsServer", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 21, "S21", "ProcessSurrenders", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 22, "S22", "AdvanceAIRebellion", PhaseStatus::Stub,
|
|
"steps an in-progress AI rebellion; the rebellion object is opaque on the wire"},
|
|
{Driver::Strategic, 23, "S23", "ScriptHookTurnStart", PhaseStatus::Stub,
|
|
"scripted-scenario callback pair; dead in a normal game but not proven so"},
|
|
{Driver::Strategic, 24, "S24", "SensorUpdate", PhaseStatus::Stub,
|
|
"packs 2-bit per-player visibility into every system and fleet"},
|
|
{Driver::Strategic, 25, "S25", "ScriptHookPostSensor", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 26, "S26", "RefreshPlayerViews", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 27, "S27", "RecomputePlayerReports", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 28, "S28", "GrantMetSpeciesTechs", PhaseStatus::Stub,
|
|
"the 'you have met this race, its racial tech appears in your tree' rule -- small, pure, "
|
|
"draw-free, and the cheapest unimplemented phase in this table"},
|
|
{Driver::Strategic, 29, "S29", "SystemObservedStamp", PhaseStatus::Implemented,
|
|
"the system's own last-observed turn, written wherever some player can currently see "
|
|
"the system and left alone everywhere else. Whole function modelled; the gate is the "
|
|
"active-presence mask, which falls when the last fleet leaves"},
|
|
{Driver::Strategic, 30, "S30", "BuildTeamPartition", PhaseStatus::Stub, ""},
|
|
{Driver::Strategic, 31, "S31", "EncounterDetectionAndStatusRestore", PhaseStatus::Partial,
|
|
"trade-raid generation runs first here and IS modelled: two chances per player, one word "
|
|
"each, neither site inside a back edge, so the count is a bound -- it is the turn's "
|
|
"dominant generator cost and it is committed under --commit-rng. Detection proper is not "
|
|
"modelled and spends two further words. The player-status restore that follows it IS "
|
|
"modelled -- it writes 1 -- but the value the file carries is 4, so a further writer "
|
|
"between this phase and the autosave is unaccounted. Committing the 1 turned two agreeing "
|
|
"leaves into disagreeing ones on the turn2->turn3 pair, so it is evaluated and reported"},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// ServerPlayer::ProcessTurn -- 12 phases, 1..12
|
|
// ---------------------------------------------------------------------------------------
|
|
constexpr PhaseDesc kPlayer[] = {
|
|
{Driver::Player, 1, "P01", "ComputeBudget", PhaseStatus::Partial,
|
|
"the formula is compared clean against the live game -- lane L5, 3,895 calls on VM146, 0 "
|
|
"divergences, 0 undeclared writes -- over 5 distinct treasury states, TWO of them on the "
|
|
"interest rounding boundary, and with a control build that proves the compare can see "
|
|
"the boundary (the exact-decimal constants produce 1,359 divergences on the same run). "
|
|
"The game fills savingsInterest with 499 at a treasury of 50,000, so G3's widened-float "
|
|
"reading is now measured, not inferred; the one-money error also moves `available` and "
|
|
"`researchMoney`. TWO further rule-23 constants in this same routine are corrected and "
|
|
"UNVERIFIED: the research-yield factor is a widened 0.85f (needs a research money that "
|
|
"is a multiple of 40,000; the run presented 9 distinct values and none is) and the three "
|
|
"research modifiers are summed in single precision (needs shrm or TRM non-zero; the "
|
|
"corpus has both at 0). The per-system money "
|
|
"input is now modelled on the TURN path -- ComputeOutput with the system's own rate "
|
|
"sliders, so the build queue, the ship-repair pass and the infrastructure -> terraform "
|
|
"-> money cascade are all live, none of which is the max-income form T31 sums. What is "
|
|
"still missing is upstream, not here: the repair demand of ships in orbit is taken as 0 "
|
|
"(its two design fields are cached stats, nowhere on the wire; the phase now reports the "
|
|
"candidate set that evidences the zero). S11's civilian growth IS committed as of G3, so "
|
|
"a colony that grew this turn is priced from its post-growth population. The phase "
|
|
"self-checks every run by "
|
|
"running the same colonies through the projected path, which the save's own BnkEl "
|
|
"states"},
|
|
{Driver::Player, 2, "P02", "ApplyNetToSavings", PhaseStatus::Partial,
|
|
"saturating add of the budget net into savings, committed. EXACT for the human and for "
|
|
"the independent colony on BOTH reference pairs now that S11 commits civilian growth "
|
|
"and the two interest rates are the widened float literals the image holds. Still "
|
|
"wrong for an AI whose research rate and target are set by its own orders during the "
|
|
"turn (Rung B). The two interest rates are now verified against the running game "
|
|
"(lane L5): the game pays 499 on 50,000 and 380 on 38,100, and the exact decimals pay "
|
|
"500 and 381"},
|
|
{Driver::Player, 3, "P03", "RecordBudgetDerivedFields", PhaseStatus::Blocked,
|
|
"trade income, savings-given-away and research-points-given-away land on the turn record "
|
|
"and on two player words that are not identified on the wire"},
|
|
{Driver::Player, 4, "P04", "ProcessSpecialProjectsSpend", PhaseStatus::Stub,
|
|
"special-project spend; the project bodies are opaque on the wire"},
|
|
{Driver::Player, 5, "P05", "ProcessResearch", PhaseStatus::Blocked,
|
|
"the research slice is verified end to end (35 live calls, 0 divergences) and P01 now "
|
|
"supplies the allocation, but the blocker has MOVED rather than cleared: the only "
|
|
"corpus player that reaches this phase with a research target is the AI, and its "
|
|
"research rate and target are set by its own orders during the same turn, so the "
|
|
"allocation fed in would be wrong. Evaluated and reported, not committed, until AI "
|
|
"order generation exists"},
|
|
{Driver::Player, 6, "P06", "ResearchRefund", PhaseStatus::Blocked,
|
|
"unspent research points converted back to money at the turn's own rate; needs P05, "
|
|
"which is now blocked on the AI's orders rather than on the budget"},
|
|
{Driver::Player, 7, "P07", "ClearTimedResearchAccumulators", PhaseStatus::Partial,
|
|
"zeroes the three timed-research accumulators that are on the wire; two further words the "
|
|
"phase also zeroes are not identified"},
|
|
{Driver::Player, 8, "P08", "DecayRebellionOutputModifier", PhaseStatus::Implemented,
|
|
"rebel AI only: the output modifier walks down by 0.04f per turn, clamped to [1, 2]"},
|
|
{Driver::Player, 9, "P09", "AccumulateTimedResearchBonuses", PhaseStatus::Implemented,
|
|
"the timed research-bonus vector, iterated from the LAST element down to index 0 -- the "
|
|
"descending order is load-bearing because float addition is not associative"},
|
|
{Driver::Player, 10, "P10", "ConsumeResearchRollPending", PhaseStatus::Partial,
|
|
"the flag/threshold test and the flag clear are implemented and committed; the draw it "
|
|
"fires is counted into the RNG ledger but the generator state is only written back under "
|
|
"--commit-rng, because the turn's other draws are not yet attributed"},
|
|
{Driver::Player, 11, "P11", "PostNoResearchEvent", PhaseStatus::Blocked,
|
|
"posting IS wired now: the event goes into the player's own turn bucket through "
|
|
"game::events, with the id sequence, the per-bucket dedup, the FLT_MAX position sentinel "
|
|
"and EvAct=1. Three of the original's four inputs are computed from the wire -- no "
|
|
"research target, nothing researched on this turn or later (the test that keeps the "
|
|
"event off the turn a tech lands), and at least one available tech, which is what "
|
|
"excludes the monster factions whose whole tree is researched. Two things keep it "
|
|
"blocked and neither is the posting. First the displayed text: the engine carries only "
|
|
"the EVENTSUM_/EVENTMSG_ keys, so a run without a data root resolves them to nothing and "
|
|
"the phase refuses to write a record it cannot fill. Second the target: the save's "
|
|
"ResTNm is the target the FILE was written with, and three of the four real players on "
|
|
"the reference pair acquire one during the turn -- that is AI research selection, and "
|
|
"--ai-player N stands in for it as a stated hypothesis. Measured with "
|
|
"--commit-blocked=P11 --data ROOT --ai-player 1 --ai-player 2 --ai-player 3: 4 leaves "
|
|
"closed on turn1->turn2 and 3 on turn2->turn3, 0 regressed. Without the roster the phase "
|
|
"over-fires by three players; that run is the control and it is in docs/EV-events.md"},
|
|
{Driver::Player, 12, "P12", "PruneRaidTargets", PhaseStatus::Stub,
|
|
"20-turn ageing of the raid-target list; the records are opaque on the wire"},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// StrategyServer::OnAllCombatDone_Tail -- 37 phases, 0..36.
|
|
//
|
|
// This is the driver the autosave is written from: everything below runs AFTER combat and
|
|
// BEFORE the file hits disk. Nothing here is implemented. It is listed in full because a
|
|
// reimplementation that reproduces the spine exactly and stops will still diverge -- two of
|
|
// these phases draw from the same generator.
|
|
// ---------------------------------------------------------------------------------------
|
|
constexpr PhaseDesc kTail[] = {
|
|
{Driver::Tail, 0, "T00", "IncrementModCount", PhaseStatus::Implemented,
|
|
"the same modification counter the spine's phase 0 bumps"},
|
|
{Driver::Tail, 1, "T01", "ValidateEncounterResultArity", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 2, "T02", "ResolveFirstContact", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 3, "T03", "AnnounceEncounterSightings", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 4, "T04", "TallyBattlesFought", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 5, "T05", "UpdateDiplomacyStatsFromCombat", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 6, "T06", "ApplyEncounterResults", PhaseStatus::Stub,
|
|
"DRAWS RNG -- node-cannon and salvage paths; the word count is combat-dependent"},
|
|
{Driver::Tail, 7, "T07", "ClearEncounters", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 8, "T08", "ScriptHookCombatDone", PhaseStatus::Stub,
|
|
"script-object event 8. Five of the twelve classes our saves carry override it -- von "
|
|
"Neumann, refugees, traps, crow defenders, independent systems -- and NONE of them moves "
|
|
"a leaf that diverges on either reference pair, so this phase is listed and not run"},
|
|
{Driver::Tail, 9, "T09", "AdvanceAIRebellionPostCombat", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 10, "T10", "NodeSpaceTravelSecondPass", PhaseStatus::Stub,
|
|
"node-space travel runs a SECOND time this turn"},
|
|
{Driver::Tail, 11, "T11", "DecayNodeLines", PhaseStatus::Stub,
|
|
"DRAWS RNG -- exactly one unit draw per expired node line per turn"},
|
|
{Driver::Tail, 12, "T12", "DrainColonyLossQueue", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 13, "T13", "UpdateTreasuryMorale", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 14, "T14", "UpdateForeignFleetMorale", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 15, "T15", "ProcessBankruptcy", PhaseStatus::Stub,
|
|
"the decision function is modelled in game::sim; the per-player state it reads is not "
|
|
"assembled here"},
|
|
{Driver::Tail, 16, "T16", "ResolveArrivedColonizers", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 17, "T17", "RebuildPlayerViewTree", PhaseStatus::Partial,
|
|
"the per-(system, player) observation record IS modelled and committed -- who saw "
|
|
"the system, on what turn, and what encounter was there. The colony-numbers view "
|
|
"the same phase rebuilds beside it is NOT: its list is empty on both reference "
|
|
"pairs, so nothing here has evidence to build it against"},
|
|
{Driver::Tail, 18, "T18", "PostFleetWarnings", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 19, "T19", "DrainInfraTerraformQueue", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 20, "T20", "ScriptHooksTurnEnd", PhaseStatus::Partial,
|
|
"two script-object event deliveries, 0x14 then 0x15. The slavers-refuel difficulty tier "
|
|
"IS modelled and committed: a three-record table scanned against the frame, boundaries at "
|
|
"1/50/100, written only when it changes -- and at frame 100 and above the scan runs off "
|
|
"the end and writes nothing, so the tier can never reach 2. The per-system pass the "
|
|
"original enters after a tier change is not modelled. Event 0x15 is overridden by no "
|
|
"class in any save we hold"},
|
|
{Driver::Tail, 21, "T21", "UpdateSurveyAndSystemStats", PhaseStatus::Partial,
|
|
"the explored sweep IS modelled and committed: every player who can currently see a "
|
|
"system has now surveyed it. The event this owes per newly-surveyed pair is not "
|
|
"posted, and the derived per-system defence figure the same phase computes is not "
|
|
"modelled"},
|
|
{Driver::Tail, 22, "T22", "TradeSliderFinalisationSecondPass", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 23, "T23", "TradeManagerEndOfTurnHooks", PhaseStatus::Stub,
|
|
"eight vtable calls, wholly unidentified"},
|
|
{Driver::Tail, 24, "T24", "RecomputeMaintenanceAndResearchBonus", PhaseStatus::Stub,
|
|
"recomputes per-player ship maintenance and research bonus and rebuilds the ship records"},
|
|
{Driver::Tail, 25, "T25", "SensorUpdateSecondPass", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 26, "T26", "ScriptHookPostSensorTail", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 27, "T27", "RefreshPlayerViewsSecondPass", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 28, "T28", "UpdateNodeLineSightingMasks", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 29, "T29", "AbortInvisibleInterceptOrders", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 30, "T30", "RebuildCommunicationMasks", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 31, "T31", "UpdateBankruptcyLimits", PhaseStatus::Partial,
|
|
"BnkEl is modelled and committed; BnkPr is not. The chain is the sum over owned, "
|
|
"non-abandoned systems of max(ComputeMaxIncome, 0). CORRECTION (lane PL): this phase used "
|
|
"to claim it self-checked against the BnkEl the input save carries, and it did not -- it "
|
|
"compared its POST-turn computation against the PRE-turn stored value, so the only "
|
|
"players it could ever 'reproduce' were the six whose limit does not move, and its "
|
|
"6-of-8 was thin coverage of exactly the shape rule 15 describes. The check is now taken "
|
|
"at LOAD, from the colony state the save was written from, and it passes 8 of 8 on the "
|
|
"reference saves and for every live player of all 11 corpus saves. That same check "
|
|
"replaces the operator flag: `ServerPlayer+0xf9` (is this player AI?) is not on the wire, "
|
|
"but it is worth 10% of the max income and BnkEl has a 6.67x slope, so recomputing the "
|
|
"stored limit under both difficulty columns identifies which one was used -- 1 AI and 1 "
|
|
"non-AI on every corpus save, the rest ambiguous because both columns agree there (zero "
|
|
"income, or an NPC, whose row is gated on `isAI && !npc`). A player neither column "
|
|
"reproduces abstains and keeps its pass-through leaf. Measured with NO operator input: 2 "
|
|
"closed, 0 regressed on EACH reference pair. VERIFIED AGAINST THE RUNNING GAME (lane "
|
|
"L5, VM146): the live `ServerPlayer+0xf9` and NPC flags, read on all eight players of "
|
|
"turn1-state across 3,895 calls, are exactly what this inversion claims -- one player "
|
|
"is AI-and-not-NPC, the human is neither, six are NPCs, and the awkward case (a player "
|
|
"that owns a system and is still ambiguous) reads npc=true, which is the reason lane "
|
|
"PL gave after its own prediction missed it. aidf (+0x368) reads 1 on all eight. What "
|
|
"is left is BnkPr, which needs BANKRUPTCY_PROTECTION_LIMIT_FACTOR from the data files "
|
|
"-- it is read as a 32-bit operand, so it is a float32 in the image and the engine now "
|
|
"narrows it (rule 23). Its RUN-TIME VALUE is no longer an assumption either: read live "
|
|
"through its pointer slot it is 3.29999995231628, i.e. (float)3.3, so the data file "
|
|
"carries 3.3 and the narrowing is real. The file image of that slot is zero, so this "
|
|
"is a fact only a running process could supply"},
|
|
{Driver::Tail, 32, "T32", "PostIncomingFleetWarnings", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 33, "T33", "ShipManagerEndOfTurnHooks", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 34, "T34", "RecordObservedDesigns", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 35, "T35", "RebuildPlayerReports", PhaseStatus::Stub, ""},
|
|
{Driver::Tail, 36, "T36", "FinalizeTurnRecords", PhaseStatus::Blocked,
|
|
"fills every player's turn record and archives it by turn; must stay last. Thirteen of "
|
|
"its fields are modelled and self-checked every run against the record the input save "
|
|
"already carries for its own turn: seven from the wire -- turn, colony count, savings, "
|
|
"the savings delta, the completed-tech count, the summed population and the alliance mask "
|
|
"phase S04 rebuilt -- and, when a data root is given, the six per-hull-class ship counts, "
|
|
"which need each design's hull size and defence-platform flag from the section catalog "
|
|
"because neither is on the wire. Only 32 of the 480 census leaves the corpus archives are "
|
|
"nonzero, so cls1 (cruisers) and cls2's platform count are UNEXERCISED, not verified. "
|
|
"Still not committed, and now blocked on exactly two named things, neither of them here: "
|
|
"savings and the income derived from it come from P01/P02, which are blocked on the "
|
|
"per-system money output; and a ship the turn BUILDS never enters our fleet list, so the "
|
|
"census carries the pre-construction count. Measured with --commit-blocked=T36 on "
|
|
"turn1->turn2: 29 leaves closed, 7 regressed -- sav x3, inc x3 and one shpt[0] short by "
|
|
"exactly the one destroyer that turn completes. The archived record is one struct on the "
|
|
"wire, so those words cannot be omitted while the rest is written: committing is "
|
|
"all-or-nothing at the record"},
|
|
};
|
|
|
|
PhaseTally Tally(const PhaseDesc* p, std::size_t n) {
|
|
PhaseTally t;
|
|
t.total = static_cast<int>(n);
|
|
for (std::size_t i = 0; i < n; ++i) {
|
|
switch (p[i].status) {
|
|
case PhaseStatus::Verified: ++t.verified; break;
|
|
case PhaseStatus::Implemented: ++t.implemented; break;
|
|
case PhaseStatus::Partial: ++t.partial; break;
|
|
case PhaseStatus::Blocked: ++t.blocked; break;
|
|
case PhaseStatus::Stub: ++t.stub; break;
|
|
}
|
|
}
|
|
return t;
|
|
}
|
|
} // namespace
|
|
|
|
const PhaseDesc* HostPhases(std::size_t& count) {
|
|
count = sizeof(kHost) / sizeof(kHost[0]);
|
|
return kHost;
|
|
}
|
|
const PhaseDesc* StrategicPhases(std::size_t& count) {
|
|
count = sizeof(kStrategic) / sizeof(kStrategic[0]);
|
|
return kStrategic;
|
|
}
|
|
const PhaseDesc* PlayerPhases(std::size_t& count) {
|
|
count = sizeof(kPlayer) / sizeof(kPlayer[0]);
|
|
return kPlayer;
|
|
}
|
|
const PhaseDesc* TailPhases(std::size_t& count) {
|
|
count = sizeof(kTail) / sizeof(kTail[0]);
|
|
return kTail;
|
|
}
|
|
|
|
PhaseTally TallySpine() {
|
|
std::size_t ns = 0, np = 0;
|
|
const PhaseDesc* s = StrategicPhases(ns);
|
|
const PhaseDesc* p = PlayerPhases(np);
|
|
PhaseTally a = Tally(s, ns), b = Tally(p, np);
|
|
PhaseTally t;
|
|
t.total = a.total + b.total;
|
|
t.verified = a.verified + b.verified;
|
|
t.implemented = a.implemented + b.implemented;
|
|
t.partial = a.partial + b.partial;
|
|
t.blocked = a.blocked + b.blocked;
|
|
t.stub = a.stub + b.stub;
|
|
return t;
|
|
}
|
|
|
|
PhaseTally TallyTail() {
|
|
std::size_t n = 0;
|
|
const PhaseDesc* p = TailPhases(n);
|
|
return Tally(p, n);
|
|
}
|
|
|
|
} // namespace sots::app
|