#include "app/script_phase.h" #include #include #include "game/sim/scriptobjects.h" namespace sots::app { namespace { using mars::stream::shapes::EncounterObject; using mars::stream::shapes::SaveGame; using mars::stream::shapes::ScriptObjects; std::string fmt(const char* f, ...) { char buf[512]; va_list ap; va_start(ap, f); std::vsnprintf(buf, sizeof buf, f, ap); va_end(ap); return std::string(buf); } // The encounter body for one id, or null. Ids the save does not carry are normal: the // factory has 23 slots and no game instantiates them all. EncounterObject* Encounter(SaveGame& game, std::int32_t encID) { if (!game.sim.svSctOb) return nullptr; ScriptObjects& root = *game.sim.svSctOb; for (auto& e : root.encounters) if (e.id == encID) return &e.obj; return nullptr; } // Every system's id and its owning-scenario tag, in save order -- which is the order the // original visits them in, and therefore the order it would draw in. void SystemScenarioTags(const SaveGame& game, std::vector& ids, std::vector& tags) { ids.reserve(game.sim.systems.size()); tags.reserve(game.sim.systems.size()); for (const auto& e : game.sim.systems) { ids.push_back(e.sysID); tags.push_back(e.sys.eggScio); } } } // namespace ScriptPhaseResult RunScriptTurnBegin(SaveGame& game, bool registerHives) { ScriptPhaseResult r; if (!game.sim.svSctOb) { r.notes.push_back("this save carries no script-object tree; nothing to deliver to"); return r; } const int frame = game.sim.frame; // --- EncID 20, refugees: the one-shot latch ------------------------------------------ if (EncounterObject* enc = Encounter(game, 20)) { ++r.objectsVisited; const sim::RefugeesTurnBeginResult res = sim::RefugeesTurnBegin(enc->refugees.ini); if (res.latched) { r.leafWrites += 1; r.notes.push_back("refugees: ini False -> True, the one-shot latch the original " "sets on the first turn it is notified"); } else { r.notes.push_back("refugees: already latched, so this delivery is a no-op -- " "which is the rule, not a gap"); } if (res.convoyOwed && res.latched) r.notes.push_back("NOT MODELLED behind the same latch: the refugee-trader convoy. " "The original instantiates a design from the data files and " "appends its handle to `dids`; on the reference pair that is " "design 1712, and the same turn also allocates ship 1728 and " "fleet 1744 (NMnx 106 -> 109). Nothing here allocates handles, " "so `didc`/`did` stay where the input save left them"); } // --- EncID 10, swarm queen: register, prune, tick ------------------------------------- if (EncounterObject* enc = Encounter(game, 10)) { ++r.objectsVisited; std::vector sysIds, tags; SystemScenarioTags(game, sysIds, tags); std::vector hives; hives.reserve(enc->swarmQueen.hives.size()); for (const auto& h : enc->swarmQueen.hives) hives.push_back(sim::Hive{h.hiveID, h.queenID, h.nextQ}); const std::size_t before = hives.size(); const std::vector owed = sim::HivesToRegister(sysIds, tags, hives); const std::vector registered = registerHives ? owed : std::vector{}; for (std::int32_t sysId : registered) { sim::Hive h; h.systemId = sysId; h.queenId = 0; // The original's target turn is `frame + LO + draw(HI - LO)`, one draw from the // strategic generator per new hive, with LO and HI two data-file constants. The // standalone models neither the constants nor the generator position at this // point in the turn, so the field is left at the frame it was registered on and // is KNOWN WRONG. It is written rather than skipped because the hive's identity // -- which system, and that it has no queen -- is exact, and that is the part // the save's structure records. h.nextQueenTurn = frame; hives.push_back(h); } const int pruned = sim::PruneHives(hives, sysIds, tags); const sim::HiveTickResult tick = sim::TickHives(hives, frame); bool changed = hives.size() != enc->swarmQueen.hives.size(); if (!changed) { for (std::size_t i = 0; i < hives.size(); ++i) { const auto& a = hives[i]; const auto& b = enc->swarmQueen.hives[i]; if (a.systemId != b.hiveID || a.queenId != b.queenID || a.nextQueenTurn != b.nextQ) { changed = true; break; } } } if (changed) { enc->swarmQueen.hives.clear(); enc->swarmQueen.hives.reserve(hives.size()); for (const sim::Hive& h : hives) { mars::stream::shapes::SVSOSwarmQueenHive w; w.hiveID = h.systemId; w.queenID = h.queenId; w.nextQ = h.nextQueenTurn; enc->swarmQueen.hives.push_back(w); } // The count is one leaf; each hive contributes its three. r.leafWrites += 1 + 3 * static_cast(hives.size()); } r.notes.push_back(fmt("swarm queen: %d hive(s) before, %d owed, %d registered, " "%d pruned, %d slipped a turn, %d now", static_cast(before), static_cast(owed.size()), static_cast(registered.size()), pruned, tick.slipped, static_cast(hives.size()))); if (!owed.empty() && !registerHives) r.notes.push_back(fmt("%d hive(s) are OWED and not written. Which systems they sit " "on and that they have no queens are exact; their target " "turn is not, and writing them would close four leaves and " "open two carrying a number known to be wrong. " "--commit-blocked=H03 takes that trade", static_cast(owed.size()))); if (!registered.empty()) r.notes.push_back("KNOWN WRONG on the hives registered here: `NextQ`. The " "original draws it -- frame + LO + draw(HI - LO), one draw per " "new hive, from the strategic generator, inside the turn-begin " "step. That is OUTSIDE both turn drivers and before either of " "them, and the campaign's measured RNG ledger (18-22 words, " "residual zero) was taken on turns where the hives already " "existed, so it has never seen this draw. Which system each " "hive sits on, and that it has no queen, ARE exact"); if (tick.slipped > 0) r.notes.push_back("the tick took the arm every corpus save takes: the spawn gates " "fail and the target turn slips forward by exactly one. That " "single increment is why the field reads 31 after turn 1 and 32 " "after turn 2 -- it walks, it is not re-rolled"); if (tick.spawnsOwed > 0) r.notes.push_back(fmt("NOT MODELLED: %d queen spawn(s). No hive in any corpus " "save has ever had a queen", tick.spawnsOwed)); } if (r.objectsVisited == 0) r.notes.push_back("neither the refugees nor the swarm queen is in this save's tree"); return r; } ScriptPhaseResult RunScriptTurnEnd(SaveGame& game) { ScriptPhaseResult r; if (!game.sim.svSctOb) { r.notes.push_back("this save carries no script-object tree; nothing to deliver to"); return r; } const int frame = game.sim.frame; // --- EncID 9, slavers refuel: the difficulty tier ------------------------------------- if (EncounterObject* enc = Encounter(game, 9)) { ++r.objectsVisited; const int stored = enc->slavers.cdiff; const sim::SlaverTierResult res = sim::SlaversTurnEnd(stored, frame); if (res.wrote) { enc->slavers.cdiff = res.tier; r.leafWrites += 1; r.notes.push_back(fmt("slavers: CDiff %d -> %d at frame %d (tier boundaries " "1/50/100)", stored, res.tier, frame)); r.notes.push_back("NOT MODELLED, and it runs only when the tier changes: the " "per-system pass the original enters after this store. It " "writes nothing this object serialises -- NAsg, NTD and NAD are " "unchanged across both reference pairs -- so its effect " "elsewhere is a labelled hypothesis, and a regression outside " "SvSctOb is what would falsify it"); } else if (sim::SlaverDifficultyTier(frame) < 0) { r.notes.push_back(fmt("slavers: at frame %d the tier scan writes nothing. Below 1 " "the first threshold already exceeds the frame; at 100 and " "above the scan runs off the end of a three-record table, " "so the tier can never reach 2 by this path", frame)); } else { r.notes.push_back(fmt("slavers: CDiff already %d at frame %d; the store is " "conditional on a change", stored, frame)); } } if (r.objectsVisited == 0) r.notes.push_back("the slavers-refuel encounter is not in this save's tree"); r.notes.push_back("the second delivery this tail step makes (event 0x15) is overridden by " "NO class in any save we hold -- it is dead on this corpus, and that is " "read from the vtables, not inferred from the bytes"); return r; } } // namespace sots::app