`SvSctOb` is not written by direct calls. Every update goes through an event bus: a
driver notifies the root object with an integer id, the root fans the delivery out to
every child, and each delivery is two steps -- a generic handler that takes the id, then
one event-specific vtable slot that does not. The id -> slot map is a 33-entry jump table
in the image, so which class reacts to which event is recovered and exhaustive rather than
inferred from what the saves happen to show. Five of the 33 rows are not in slot order,
including two the tail sends.
Three handlers write the eight leaves that diverged:
* the slavers' difficulty tier, on the tail's end-of-turn delivery -- a three-record
stack table scanned against the frame, boundaries 1/50/100, stored only on a change,
and at frame 100 and above the scan runs off the end and stores nothing, so the tier
can never reach 2;
* the refugees' one-shot latch, on the turn-begin delivery, with a design instantiation
behind the same latch that nothing here can do;
* the swarm queen's hives, also at turn begin, registered on the systems carrying the
SWARM's scenario tag (the queen's constructor stores 3 for that and 10 for its own
encounter id) and then ticked -- and the tick is the whole explanation of a target
turn that reads 31 after one turn and 32 after the next. It is not re-rolled; it slips
forward by one every turn the spawn gates stay shut.
New host phase H03 for the turn-begin delivery, run right after the frame counter where
the original sends it, and tail phase T20 implemented. Rules are pure in game/sim.
Measured on CT111, closed and regressed stated separately:
default turn1->turn2 209 -> 126 (was 128) closed 83, regressed 0
turn2->turn3 108 -> 67 (was 69) closed 41, regressed 0
--commit-blocked=H03 turn1->turn2 209 -> 124 closed 87, regressed 2
turn2->turn3 108 -> 67 closed 41, regressed 0
Registering a hive closes the four leaves that say which systems have hives and that they
have no queens, and opens two carrying a target turn known to be wrong: the original draws
it from the strategic generator inside the turn-begin step, outside both turn drivers, and
neither the two data-file constants nor the generator's position there is settled. That
trade is a flag, not a default.
The prediction in docs/SV-script-objects.md was committed before the build, and P5 was
wrong: it called the second pair a null control, and the second pair is where the slip
rule is tested EXACTLY -- two hives, two target turns, both landing on the oracle with no
draw and no fitting.
Gates run separately: clean-room OK, host ctest 51/51, CT111 shim cross-build clean.
213 lines
10 KiB
C++
213 lines
10 KiB
C++
#include "app/script_phase.h"
|
|
|
|
#include <cstdarg>
|
|
#include <cstdio>
|
|
|
|
#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<std::int32_t>& ids,
|
|
std::vector<std::int32_t>& 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<std::int32_t> sysIds, tags;
|
|
SystemScenarioTags(game, sysIds, tags);
|
|
|
|
std::vector<sim::Hive> 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<std::int32_t> owed = sim::HivesToRegister(sysIds, tags, hives);
|
|
const std::vector<std::int32_t> registered =
|
|
registerHives ? owed : std::vector<std::int32_t>{};
|
|
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<int>(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<int>(before), static_cast<int>(owed.size()),
|
|
static_cast<int>(registered.size()), pruned, tick.slipped,
|
|
static_cast<int>(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<int>(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
|