Compare commits

..

No commits in common. "52db23ce585e3395638e82af0608f5aa6ad014db" and "e7e2bd6f628946aed7ebf62a91106d6592d16114" have entirely different histories.

14 changed files with 45 additions and 490 deletions

View file

@ -961,154 +961,13 @@ struct PrEntry {
ar.i32(A("PRBt"), prbt);
}
};
// ---- special projects: Game::SpecialProjectImpl and the four subclasses -----------
//
// `Sprj` is a POLYMORPHIC frame and `SprjT`, written immediately before it, is the
// discriminator. Both halves of the mapping are measured, not guessed:
//
// * ServerPlayer::Write (0x856fe4) writes `SprjT` from the plain member at
// `project+0x3c` -- not from a virtual call -- and then writes the object as
// the `Sprj` frame;
// * ServerPlayer::Read (0x881aa7) reads `SprjT` and passes it as the first
// argument to the factory at 0x8610a0, whose 4-entry jump table (0x8611e8) is
//
// 0 -> new 0x54, ctor(0, ...), vftable 0xa3185c Game::BackEngProject
// 1 -> new 0x54, ctor(...), vftable 0xa3180c Game::MonitorProject
// 2 -> new 0x4c, ctor(...), vftable 0x9fa438 Game::JewelsProject
// 3 -> new 0x60, ctor(3, ...), vftable 0xa31884 Game::TechOfferProject
//
// and each of those constructors stores that same index at `+0x3c`, which
// closes the loop: the value the writer emits is the index the factory takes.
// Any other value creates nothing, so a save carrying one is malformed.
//
// Game::TechProject (vftable 0xa31834) has NO factory entry and cannot come off a
// save; it shares BackEngProject's serializer (Read 0x853280 / Write 0x853390)
// byte for byte, so the two are indistinguishable on the wire anyway.
//
// ONLY SprjT 0 IS EXERCISED by the corpus (one record, in the 12 `ad-*`/`ar-*`/`bp-*`
// saves: a back-engineering project on "Magonian Prophicies" for `DRV_RecFiss`).
// The other three arms are typed from the recovered schema and the factory, and
// are HYPOTHESES until a save exercises them (rule 6). An unknown SprjT falls to
// `rest()`, which shows up as opaque coverage rather than as a silent mis-read.
struct SpecialProject { // Game::SpecialProject -- the `SPi` sub-frame, common to all four
static constexpr const char* kStreamName = "SPi";
int32_t spid = 0, sts = 0;
float cst = 0;
int32_t mxc = 0;
std::string nm;
int32_t trns = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("SPid"), spid);
ar.i32(A("Sts"), sts);
ar.f32(A("Cst"), cst);
ar.i32(A("MxC"), mxc);
ar.str(A("Nm"), nm);
ar.i32(A("Trns"), trns);
}
};
struct BackEngProject { // Game::BackEngProject (SprjT 0); Game::TechProject shares this serializer
static constexpr const char* kStreamName = "Sprj";
int32_t stp = 0;
SpecialProject spi;
float aOdd = 0, aInc = 0;
std::string tch;
int32_t rCst = 0, rDn = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("Stp"), stp);
ar.obj(A("SPi"), spi);
ar.f32(A("AOdd"), aOdd);
ar.f32(A("AInc"), aInc);
ar.str(A("Tch"), tch);
ar.i32(A("RCst"), rCst);
ar.i32(A("RDn"), rDn);
}
};
struct MonitorProject { // Game::MonitorProject (SprjT 1) -- no save exercises it
static constexpr const char* kStreamName = "Sprj";
int32_t stp = 0;
SpecialProject spi;
int32_t sys = 0, rMn = 0, rMx = 0, rDn = 0;
float rMd = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("Stp"), stp);
ar.obj(A("SPi"), spi);
ar.i32(A("Sys"), sys);
ar.i32(A("RMn"), rMn);
ar.i32(A("RMx"), rMx);
ar.i32(A("RDn"), rDn);
ar.f32(A("RMd"), rMd);
}
};
struct JewelsProject { // Game::JewelsProject (SprjT 2) -- no save exercises it
static constexpr const char* kStreamName = "Sprj";
int32_t stp = 0;
SpecialProject spi;
int32_t einv = 0, eapp = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("Stp"), stp);
ar.obj(A("SPi"), spi);
ar.i32(A("einv"), einv);
ar.i32(A("eapp"), eapp);
}
};
struct TechOfferProject { // Game::TechOfferProject (SprjT 3) -- no save exercises it
static constexpr const char* kStreamName = "Sprj";
int32_t stp = 0;
SpecialProject spi;
float aOdd = 0, aInc = 0;
std::string tch;
int32_t rCst = 0, rDn = 0;
int32_t giv = 0, rcp = 0;
bool rcpd2 = false;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("Stp"), stp);
ar.obj(A("SPi"), spi);
ar.f32(A("AOdd"), aOdd);
ar.f32(A("AInc"), aInc);
ar.str(A("Tch"), tch);
ar.i32(A("RCst"), rCst);
ar.i32(A("RDn"), rDn);
ar.i32(A("giv"), giv);
ar.i32(A("rcp"), rcp);
ar.b(A("rcpd2"), rcpd2);
}
};
// The `Sprj` body, selected by the `SprjT` beside it. Same idiom as EncounterObject:
// a switch, not a `when`, so SchemaProbe does not concatenate the arms -- each arm is
// bound to its own wire class in tests/mars_stream/test_wire_schema.cpp.
struct SpecialProjectBody {
static constexpr const char* kStreamName = "Sprj";
int32_t type = 0;
BackEngProject backEng; // 0
MonitorProject monitor; // 1
JewelsProject jewels; // 2
TechOfferProject offer; // 3
std::vector<Node> unknown; // no factory entry: carried, and counted as opaque
void select(int32_t sprjT) { type = sprjT; }
template <class Ar>
void io(Ar& ar) {
switch (type) {
case 0: backEng.io(ar); break;
case 1: monitor.io(ar); break;
case 2: jewels.io(ar); break;
case 3: offer.io(ar); break;
default: ar.rest(unknown); break;
}
}
};
struct SprjEntry {
int32_t sprjT = 0;
SpecialProjectBody sprj;
Node sprj;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("SprjT"), sprjT);
sprj.select(sprjT);
ar.obj(A("Sprj"), sprj);
ar.any(A("Sprj"), sprj);
}
};
struct NexpEntry {
@ -1492,37 +1351,12 @@ struct SpyManager { // Game::ServerSpyManager
ar.rest(extra);
}
};
// One used-suffix slot of a name-generator record: the `usp`/`usc` PAIR.
//
// MEASURED from the writer (Game::SpecialProjectNameGen::Write, 0x8147e0). Each
// record is 0x58 bytes and opens with a 32-byte table of per-suffix use counts.
// The writer counts the NON-ZERO entries (the unrolled byte loop at 0x814850) and
// emits that as `usnc`; it then walks slots 0..31 and, for every non-zero one,
// writes the SLOT INDEX as `usp` (0x8148b1) and that slot's count byte, widened by
// `movzx`, as `usc` (0x8148c2). Both go through WriteInt, so both are i32 on the
// wire (rule 5). `Nm` and `Ntg` follow from record+0x20 and record+0x3c.
//
// The old shape read `usnc` as a count of ONE item and named only `usc`, on the
// strength of "usnc is 0 in every save available". That was rule 6's hypothesis
// and `ad-turn27-two-raiders.sav` falsified it: 102 of its 103 records have an
// empty table and one has `usnc == 1`, so the writer dropped that record's `usp`
// and the save round-tripped exactly 12 bytes short (one i32 item: 4-byte length +
// 3-byte tag + 4-byte value + 1 pad). 12 of the 43 corpus saves carry that record.
struct UsedName {
int32_t slot = 0; // usp -- index into the record's 32-slot use table
int32_t count = 0; // usc -- that slot's use count (a byte in memory, i32 on the wire)
template <class Ar>
void io(Ar& ar) {
ar.i32(A("usp"), slot);
ar.i32(A("usc"), count);
}
};
struct ProjectName { // one entry of Game::SpecialProjectNameGen
std::vector<UsedName> used;
std::vector<Node> used; // usnc is 0 in every save available
std::string nm, ntg;
template <class Ar>
void io(Ar& ar) {
ar.narr(A("usnc"), used, [](Ar& a, UsedName& e) { e.io(a); });
ar.narr(A("usnc"), used, [](Ar& a, Node& e) { a.any(A("usc"), e); });
ar.str(A("Nm"), nm);
ar.str(A("Ntg"), ntg);
}
@ -1858,40 +1692,19 @@ struct ShipEntry {
ar.obj(A("Ship"), ship);
}
};
struct FieldPoint { // Game::FieldTemplate::Point -- one ship's slot in a stored formation
static constexpr const char* kStreamName = "";
int32_t shipID = 0, desID = 0, posX = 0, posY = 0, sqd = 0;
template <class Ar>
void io(Ar& ar) {
ar.i32(A("FTPShID"), shipID);
ar.i32(A("FTPDesID"), desID);
ar.i32(A("FTPPosX"), posX);
ar.i32(A("FTPPosY"), posY);
ar.i32(A("FTPSqd"), sqd);
}
};
struct FieldTemplate { // Game::FieldTemplate, the NULL-named first frame of a Lay
static constexpr const char* kStreamName = "";
// FTPnts was the ONE item the recovery itself marks unresolved: it names the
// element class from the decorated helper name (Game::FieldTemplate::Point,
// whose own serializer is fully recovered -- FTPShID/FTPDesID/FTPPosX/FTPPosY/
// FTPSqd) but could not type the item, and the count was 0 in every Lay in the
// 22-save corpus. The elements were therefore carried as Nodes rather than
// typed against a framing no save had ever shown -- the trap that mis-typed
// SysMem, mts and nalat.
//
// The workload that note asked for now EXISTS. Six saves in the 43-save corpus
// carry stored tactical formations (`ap-turn22` 2 points, `az-turn23` 3, and
// `ap-turn25` / `ar-oracle-A-pre` / `ar-r1-turn43-post` / `ar-turn37` 1 each),
// and the framing is the ordinary CArr: a `FTPnts` frame holding a "." count
// and that many NULL-named element frames of five i32 items. Typed, and
// byte-identical on all six.
//
// FTPPosX/FTPPosY/FTPSqd read 0 in every point observed so far, so their DISK
// type (i32, from the WriteInt vftable slot) is the schema's word and not the
// corpus's -- a formation with a ship moved off the origin is what would tell
// an i32 grid coordinate from a float one by value.
std::vector<FieldPoint> points;
// FTPnts is the ONE item in this round's work that the recovery itself marks
// unresolved: it names the element class from the decorated helper name
// (Game::FieldTemplate::Point, whose own serializer is fully recovered --
// FTPShID/FTPDesID/FTPPosX/FTPPosY/FTPSqd) but could not type the item, and
// the count is 0 in every Lay in the corpus. That is exactly the pair of
// conditions under which SysMem, mts and nalat were all typed wrong: the
// element FRAMING is a property of the helper, not of the element class, and
// no save has ever shown one. So the elements stay carried. What settles it
// is a save whose fleet has a stored tactical formation -- set a fleet's
// combat layout in the tactical setup screen, then save.
std::vector<Node> points;
std::vector<Node> extra;
template <class Ar>
void io(Ar& ar) {

View file

@ -79,11 +79,6 @@ struct Env {
std::uintptr_t exe_base = 0;
void (*log_line)(const char*) = nullptr;
CostFn cost = nullptr;
// `research.replace_cascade=on` (lane CR). Default OFF, so a build behaves exactly as it
// did before this key existed and the two replace configurations differ by configuration
// rather than by binary. See ours() for what the flag does and, just as important, what it
// deliberately still does not do.
bool replace_cascade = false;
};
Env g_env;
@ -939,29 +934,17 @@ void TechTreeProcessResearchHook::ours(void* tree, void* rng, void* alloc, int*
// completed node's turn/order stamps, the child costs and states, the availability stamp
// EVENT_TECHS_UNLOCKED is computed from, the observed-tech append, and one RNG word.
//
// It ran in COMPARE MODE ONLY until lane CR, for the same reason the event post still does:
// in replace mode every pointer here is live game memory, and running half of
// OnTechResearched -- the observed-tech append and the research-event roll, but not the
// ninety-odd tech-effect field writes -- leaves the player in a state no code path produces.
// It runs in COMPARE MODE ONLY, for the same reason the event post does. In replace mode
// every pointer here is live game memory, and running half of OnTechResearched -- the
// observed-tech append and the research-event roll, but not the ninety-odd tech-effect field
// writes -- would leave the player in a state no code path produces. Not running it leaves a
// player missing a cascade, which is a smaller and already-declared lie.
//
// `research.replace_cascade=on` opts a replace run into the half that is entirely inside this
// subsystem: the four TechNode words SetResearched stamps (costRP, turnAvailable,
// turnResearched, order) and the tree's own completion-order counter. Those are TechTree
// state, they are what the compare has been checking for a day, and writing them live is what
// SetResearched does. Everything the callback does to the *player* stays unmodelled and stays
// declared -- the event post, the ObservedTech element and the tech-effect fields -- so a
// replace run with the flag on is still a partial displacement, by exactly the boundary the
// coverage notes name. The point of the flag is to make that boundary measurable: the same
// binary, the same save, two configurations, and the leaf difference between their autosaves
// is the cascade's own contribution.
//
// The graph is transcribed from the node copies -- SCRATCH in compare mode, the live nodes in
// replace mode, where nothing has run yet -- so in both cases what the cascade decides is a
// function of the pre-call tree and not of what the original just did.
// The graph is transcribed from the SCRATCH node copies, so what the cascade decides is a
// function of the pre-call tree and not of what the original just did to the live one.
sots::sim::TechGraph graph;
CascadeCtx cc;
const bool cascade_possible =
(compare || g_env.replace_cascade) && g_scan.turn_ok && g_pre.tree_ok;
const bool cascade_possible = compare && g_scan.turn_ok && g_pre.tree_ok;
bool cascade_ok = false;
if (cascade_possible) {
cascade_ok = build_graph(nodes, graph);
@ -1006,14 +989,6 @@ void TechTreeProcessResearchHook::ours(void* tree, void* rng, void* alloc, int*
set_word(p, A::TechNode_off_TurnResearched, graph.nodes[i].turnResearched);
set_word(p, A::TechNode_off_Order, graph.nodes[i].order);
}
// The counter the `order` stamps came from. In compare mode `tree` is the LIVE tree (only
// the nodes are copied), so this must never run there -- writing it would move a byte the
// tree_header guard is there to watch, on a run whose whole value is that ours touched
// nothing. In replace mode nothing else advances it, and leaving it behind would make the
// next completion reuse an order number.
if (!compare && g_pre.tree_ok) {
set_word(tree, A::TechTree_off_OrderCounter, graph.orderCounter);
}
cc.unlocked = sots::sim::CollectNewlyAvailable(graph, g_scan.turn);
}
set_word(overbudget, 0, word_at(overbudget, 0) + r.overbudget);
@ -1119,49 +1094,22 @@ void TechTreeProcessResearchHook::ours(void* tree, void* rng, void* alloc, int*
// A line per call, so a run can be read without the trace: these are the counts that say the
// cascade actually ran, and a clean compare with all of them at zero would be a clean compare
// of nothing.
//
// `steps`/`completed` are counted from the PASS itself and are therefore printed in every
// mode, cascade or no cascade. That is the point: rule 1 says a green verdict on a hook that
// may be comparing nothing is not evidence, and the same holds for a green replace-mode
// ORACLE. Without this line a replace run with the cascade off has no counter at all -- the
// old log line was gated on the cascade -- so "a completion happened" could only ever be
// inferred from the save, which is the artefact under test. Counted here, it is a statement
// by the instrument about what our own code did.
int model_completions = 0;
for (const sots::sim::ResearchStepResult& s : r.steps)
if (s.completed) ++model_completions;
logf("research: mode=%s steps=%u completions=%d overbudget=%d cascade_possible=%d ok=%d "
"cascade_completions=%d unlocked=%u otch_appends=%d roll_draws=%d failures=%d depth=%d "
"name_unreadable=%d",
compare ? "compare" : "replace", static_cast<unsigned>(r.steps.size()), model_completions,
r.overbudget, cascade_possible ? 1 : 0, cascade_ok ? 1 : 0, cc.completions,
static_cast<unsigned>(cc.unlocked.size()), cc.observed_appends, cc.roll_draws,
cc.cascade_failures, cc.depth_exceeded ? 1 : 0, cc.name_unreadable ? 1 : 0);
}
bool research_config(const char* key, const char* value, std::string* err) {
if (std::strcmp(key, "research.replace_cascade") == 0) {
if (std::strcmp(value, "on") == 0 || std::strcmp(value, "1") == 0) {
g_env.replace_cascade = true;
} else if (std::strcmp(value, "off") == 0 || std::strcmp(value, "0") == 0) {
g_env.replace_cascade = false;
} else if (err) {
*err = "expected on|off";
}
return true;
if (cascade_possible) {
logf("research: cascade ok=%d completions=%d unlocked=%u otch_appends=%d roll_draws=%d "
"failures=%d depth=%d name_unreadable=%d",
cascade_ok ? 1 : 0, cc.completions, static_cast<unsigned>(cc.unlocked.size()),
cc.observed_appends, cc.roll_draws, cc.cascade_failures, cc.depth_exceeded ? 1 : 0,
cc.name_unreadable ? 1 : 0);
}
return false;
}
void init_research(std::uintptr_t exe_base, void (*log_line)(const char* line)) {
g_env.exe_base = exe_base;
g_env.log_line = log_line;
g_env.cost = reinterpret_cast<CostFn>(exe_base + A::TechTree_Cost);
logf("research: ProcessResearch hook ready (Cost=%p, node=0x%x, rng=0x%x, fpu_cw=0x%04x, "
"replace_cascade=%s)",
logf("research: ProcessResearch hook ready (Cost=%p, node=0x%x, rng=0x%x, fpu_cw=0x%04x)",
reinterpret_cast<void*>(g_env.cost), static_cast<unsigned>(kNodeSize),
static_cast<unsigned>(kRngSize), fpu_control_word(),
g_env.replace_cascade ? "on" : "off");
static_cast<unsigned>(kRngSize), fpu_control_word());
}
} // namespace shim::hooks

View file

@ -43,13 +43,8 @@
// and the rebellion branch cancels the research outright. Its tech-effect field writes are B2's
// milestone and remain what the `player` guard reports.
//
// REPLACE mode runs none of the cascade by default: there, every pointer is live game memory and
// applying half of the callback would leave the player in a state no code path produces.
// `research.replace_cascade=on` opts a replace run into the TechTree half of it -- the four
// TechNode words SetResearched stamps and the tree's completion-order counter -- and nothing of
// the ServerPlayer half. That is deliberate and it is measurable: the same binary run twice with
// the flag off and on gives two autosaves whose leaf difference IS the cascade's contribution,
// and what both still differ from the original by is the callback's effect on the player.
// REPLACE mode runs none of the cascade: there, every pointer is live game memory and applying
// half of the callback would leave the player in a state no code path produces.
//
// The effective cost of a node is taken from the game's own TechTree::Cost, which is read-only --
// the cost multiplier is a separate, lower-confidence formula and not what this milestone is
@ -57,7 +52,6 @@
#pragma once
#include <cstdint>
#include <string>
#include <tuple>
#include <vector>
@ -100,16 +94,13 @@ struct TechTreeProcessResearchHook {
"table, so the message is composed from node indices instead and is not the "
"game's text",
"region:events");
c.unmodelled("TechTree::SetResearched in REPLACE mode: only its TechTree half runs, and "
"only when research.replace_cascade=on",
c.unmodelled("TechTree::SetResearched in REPLACE mode: nothing of it runs",
trace::Risk::High,
"with the flag OFF (the default) nothing of the cascade runs, so a replace "
"run leaves the completed node unstamped and no tech unlocked. With it ON, "
"the four TechNode words (costRP, turnAvailable, turnResearched, order) and "
"the tree's completion-order counter are written live, and the ServerPlayer "
"half is still not: no event is posted, no ObservedTech element is appended "
"and no tech effect is applied. Neither setting is a full displacement of "
"the completion path; the pair measures where the boundary is",
"the cascade is compare-mode only. In replace mode every pointer is live "
"game memory, and applying half of OnTechResearched -- the observed-tech "
"append and the research-event roll, but not the tech-effect field writes -- "
"would leave the player in a state no code path produces. A replace run "
"therefore still leaves the completed node unstamped and no tech unlocked",
"guard:player, guard:tree_header");
c.unmodelled("ServerPlayer::OnTechResearched's tech effects: the ~90 hard-coded "
"ServerPlayer field writes, the plague-cure masks, the design-option "
@ -155,10 +146,6 @@ struct TechTreeProcessResearchHook {
}
};
// `research.replace_cascade=on|off` (default off). Returns false if `key` is not ours.
// Call BEFORE init_research so the banner reports the value that is in force.
bool research_config(const char* key, const char* value, std::string* err);
// Process facts the hook needs (exe base for the RVAs, a line logger). Call once before
// installing.
void init_research(std::uintptr_t exe_base, void (*log_line)(const char* line));

View file

@ -102,9 +102,6 @@ Config ReadConfig() {
} else if (shim::hooks::ai_orders_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::research_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);
} else if (shim::hooks::watch_apply_config(p, val, &err)) {
if (!err.empty()) Log("config: %s=%s rejected (%s)", p, val, err.c_str());
else Log("config: %s=%s", p, val);

View file

@ -116,9 +116,3 @@ aiseed.values=32=156ebbbd,496=fe7b2826,512=0ed341d1,*=deadbeef
# With `aiseed=pin` and no values the module logs "PIN MODE WITH NO PINS -- every seed passes
# through unchanged, so this run is NOT pinned and must not be reported as one". Read that, and
# the three `aiseed call=... pinned=1` lines, in shim.log before trusting any run.
# The FPU-force module installs FOUR sampling detours by default, controlled by keys that
# do not start with hook. -- lane CR found a config naming all 27 template hooks still
# installed six detours. An exhaustive config turns these off explicitly.
fpu.sample_turn=off
fpu.sample_ticks=off

View file

@ -73,9 +73,3 @@ aiseed=pin
# The seeds run C3 (unpinned, same save, same guest, same build) observed for itself. Pinning
# them reproduces a turn that actually happened rather than inventing one.
aiseed.values=32=e70a4703,496=0c63ca36,512=372be4df
# The FPU-force module installs FOUR sampling detours by default, controlled by keys that
# do not start with hook. -- lane CR found a config naming all 27 template hooks still
# installed six detours. An exhaustive config turns these off explicitly.
fpu.sample_turn=off
fpu.sample_ticks=off

View file

@ -1,41 +0,0 @@
# Lane CR, crcompare. ProcessResearch in compare; every other registered hook named off.
# research.replace_cascade=off.
# exhaustive
hooks=trace
hook.Game::EncounterDetect::AssignContacts=off
hook.Game::EncounterDetect::ProcessTeamRecord=off
hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
hook.Game::SVSOSwarmQueen::OnTurnBegin=off
hook.Game::SVSOSwarmQueen::RegisterHives=off
hook.Game::SVSOSwarmQueen::TickHives=off
hook.Game::SectionDictionary::SectionDictionary=off
hook.Game::ServerPlayer::ComputeBudget=off
hook.Game::ServerPlayer::OnTechResearched=off
hook.Game::ServerPlayer::ProcessTurn=off
hook.Game::ServerSystem::ComputeTotalOutput=off
hook.Game::ServerSystem::GroupOutput=off
hook.Game::ServerSystem::ProcessTurn=off
hook.Game::StrategyApp::RunAI=off
hook.Game::StrategyHost::Autosave=off
hook.Game::StrategyServer::ApplyEncounterResult=off
hook.Game::StrategyServer::BeginProcessTurn=off
hook.Game::StrategyServer::MoveFleet=off
hook.Game::StrategyServer::NodeLineDecay=off
hook.Game::StrategyServer::OnAllCombatDone_Tail=off
hook.Game::StrategyServer::ProcessFleetMovement=off
hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
hook.Game::StrategyServer::ProcessTurn=off
hook.Game::WeaponDictionary::Init=off
hook.Mars::GlobalConsts::LoadFile=off
hook.Mars::RNG::Seed=off
hook.Game::TechTree::ProcessResearch=compare
research.replace_cascade=off
trace.path=C:\SOTS\shim.trace.jsonl
trace.inline_max=256
trace.flush=always
# The FPU-force module installs FOUR sampling detours by default, controlled by keys that
# do not start with hook. -- lane CR found a config naming all 27 template hooks still
# installed six detours. An exhaustive config turns these off explicitly.
fpu.sample_turn=off
fpu.sample_ticks=off

View file

@ -1,6 +0,0 @@
# Lane CR, the rule-19 control. Same proxy DLL as every measured CR run, same guest, same save,
# same click path -- `hooks=off` installs NOTHING (not even the M0 asm stub), so a difference
# between this run's autosaves and a measured run's is the instrument and nothing else.
# `# exhaustive` is deliberately absent: it is a claim about a `hooks=trace` hook set, and there
# is no hook set here to be exhaustive about.
hooks=off

View file

@ -1,41 +0,0 @@
# Lane CR, crreplace0. ProcessResearch in replace; every other registered hook named off.
# research.replace_cascade=off.
# exhaustive
hooks=trace
hook.Game::EncounterDetect::AssignContacts=off
hook.Game::EncounterDetect::ProcessTeamRecord=off
hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
hook.Game::SVSOSwarmQueen::OnTurnBegin=off
hook.Game::SVSOSwarmQueen::RegisterHives=off
hook.Game::SVSOSwarmQueen::TickHives=off
hook.Game::SectionDictionary::SectionDictionary=off
hook.Game::ServerPlayer::ComputeBudget=off
hook.Game::ServerPlayer::OnTechResearched=off
hook.Game::ServerPlayer::ProcessTurn=off
hook.Game::ServerSystem::ComputeTotalOutput=off
hook.Game::ServerSystem::GroupOutput=off
hook.Game::ServerSystem::ProcessTurn=off
hook.Game::StrategyApp::RunAI=off
hook.Game::StrategyHost::Autosave=off
hook.Game::StrategyServer::ApplyEncounterResult=off
hook.Game::StrategyServer::BeginProcessTurn=off
hook.Game::StrategyServer::MoveFleet=off
hook.Game::StrategyServer::NodeLineDecay=off
hook.Game::StrategyServer::OnAllCombatDone_Tail=off
hook.Game::StrategyServer::ProcessFleetMovement=off
hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
hook.Game::StrategyServer::ProcessTurn=off
hook.Game::WeaponDictionary::Init=off
hook.Mars::GlobalConsts::LoadFile=off
hook.Mars::RNG::Seed=off
hook.Game::TechTree::ProcessResearch=replace
research.replace_cascade=off
trace.path=C:\SOTS\shim.trace.jsonl
trace.inline_max=256
trace.flush=always
# The FPU-force module installs FOUR sampling detours by default, controlled by keys that
# do not start with hook. -- lane CR found a config naming all 27 template hooks still
# installed six detours. An exhaustive config turns these off explicitly.
fpu.sample_turn=off
fpu.sample_ticks=off

View file

@ -1,41 +0,0 @@
# Lane CR, crreplace1. ProcessResearch in replace; every other registered hook named off.
# research.replace_cascade=on.
# exhaustive
hooks=trace
hook.Game::EncounterDetect::AssignContacts=off
hook.Game::EncounterDetect::ProcessTeamRecord=off
hook.Game::SVSOSlaversRefuel::UpdateDifficultyTier=off
hook.Game::SVSOSwarmQueen::OnTurnBegin=off
hook.Game::SVSOSwarmQueen::RegisterHives=off
hook.Game::SVSOSwarmQueen::TickHives=off
hook.Game::SectionDictionary::SectionDictionary=off
hook.Game::ServerPlayer::ComputeBudget=off
hook.Game::ServerPlayer::OnTechResearched=off
hook.Game::ServerPlayer::ProcessTurn=off
hook.Game::ServerSystem::ComputeTotalOutput=off
hook.Game::ServerSystem::GroupOutput=off
hook.Game::ServerSystem::ProcessTurn=off
hook.Game::StrategyApp::RunAI=off
hook.Game::StrategyHost::Autosave=off
hook.Game::StrategyServer::ApplyEncounterResult=off
hook.Game::StrategyServer::BeginProcessTurn=off
hook.Game::StrategyServer::MoveFleet=off
hook.Game::StrategyServer::NodeLineDecay=off
hook.Game::StrategyServer::OnAllCombatDone_Tail=off
hook.Game::StrategyServer::ProcessFleetMovement=off
hook.Game::StrategyServer::ProcessNodeSpaceTravel=off
hook.Game::StrategyServer::ProcessTurn=off
hook.Game::WeaponDictionary::Init=off
hook.Mars::GlobalConsts::LoadFile=off
hook.Mars::RNG::Seed=off
hook.Game::TechTree::ProcessResearch=replace
research.replace_cascade=on
trace.path=C:\SOTS\shim.trace.jsonl
trace.inline_max=256
trace.flush=always
# The FPU-force module installs FOUR sampling detours by default, controlled by keys that
# do not start with hook. -- lane CR found a config naming all 27 template hooks still
# installed six detours. An exhaustive config turns these off explicitly.
fpu.sample_turn=off
fpu.sample_ticks=off

View file

@ -124,17 +124,7 @@ int main() {
// 2026-09-09, 22 saves: 724 fields observed, 490 vary, 234 do not. Nearly a
// third of the format we call "99.99% typed" has been seen holding exactly
// one value. `tscr` was one of those 234 until a one-turn tech moved it.
//
// 2026-09-09, 43 saves (lane DW): 755 fields observed, 538 vary, 217 do not.
// The corpus doubled and the census grew by 31 fields, of which 19 are this
// lane's newly typed bodies -- the `usp`/`usc` pair of a name-generator record,
// the twelve of a `Sprj` back-engineering project, and the five of a stored
// formation point. Only two of those 19 have ever been seen to move
// (`FTPShID`, `FTPDesID`), which is exactly why the count of CONSTANTS rose
// faster than the count of varying fields: typing a body honestly adds far
// more unexercised fields than exercised ones, and this census is the place
// that stays visible.
const size_t kVaryingBaseline = 538;
const size_t kVaryingBaseline = 490;
CHECK(varying >= kVaryingBaseline);
std::printf("test_domains: %s (%zu save(s), %d failure(s))\n", fails ? "FAILED" : "ok", saves.size(), fails);

View file

@ -142,16 +142,6 @@ static void check_save(const std::string& path, const char* dump_dir) {
// With TurnCommands_v5 typed, the only items left carried as Nodes are the
// two of the MT19937 block, which is deliberately opaque.
CHECK(pct >= 99.99);
// The percentage is the WEAKER half of this ratchet and cannot be raised
// usefully: it is 2/items, so tightening it would fail on a *small* save
// that types perfectly and pass a big one that carries a new opaque body.
// The strong form is the item count itself. 2026-09-09, 43 saves: the only
// opaque items in any save are the MT19937 block's two (`RNG`), so the
// ratchet is that exact set. A newly-carried body breaks this line first,
// and it breaks it on the save that has the body, not on the smallest one.
// Raise it -- by typing the body -- the way rule 27 says; never widen it.
CHECK(cov.opaque == 2);
CHECK(cov.opaque_by_tag.size() == 1 && cov.opaque_by_tag.count("RNG") == 1);
}
// --- round trips ------------------------------------------------------------------

View file

@ -305,15 +305,6 @@ int main() {
check<sh::SpyReport>("SpyReport", "Game::SpyReport");
check<sh::SpyManager>("SpyManager", "Game::ServerSpyManager");
check<sh::ProjectNames>("ProjectNames", "Game::SpecialProjectNameGen");
// The `Sprj` variant: the SELECTION (SprjT -> class) is not a wire item and so
// is not checkable here; it came from the game's own factory at 0x8610a0 (see
// the note on sh::SpecialProjectBody). What is checked is that each arm agrees
// with its class's serializer. Only SprjT 0 is exercised by any save.
check<sh::SpecialProject>("SpecialProject", "Game::SpecialProject");
check<sh::BackEngProject>("BackEngProject", "Game::BackEngProject");
check<sh::MonitorProject>("MonitorProject", "Game::MonitorProject");
check<sh::JewelsProject>("JewelsProject", "Game::JewelsProject");
check<sh::TechOfferProject>("TechOfferProject", "Game::TechOfferProject");
check<sh::TradeManager>("TradeManager", "Game::ServerTradeManagerImpl");
check<sh::TradeSector>("TradeSector", "Game::ServerTradeSector");
check<sh::ShipSectionID>("ShipSectionID", "Game::ShipSectionID");
@ -328,11 +319,11 @@ int main() {
check<sh::TradeRoute>("TradeRoute", "Game::TradeRoute");
check<sh::SpyCraft>("SpyCraft", "Game::SpyCraft");
check<sh::FleetLayout>("FleetLayout", "Game::FleetLayout");
// Game::FieldTemplate::Point is bound now that the corpus exercises it: six
// saves carry stored tactical formations, so the element framing is measured
// rather than assumed (see the note on sh::FieldTemplate).
// Game::FieldTemplate::Point is deliberately NOT bound: FTPnts is empty in
// every save and the recovery marks the item unresolved, so its element
// framing is a hypothesis and FieldTemplate carries the elements (see the
// note on sh::FieldTemplate).
check<sh::FieldTemplate>("FieldTemplate", "Game::FieldTemplate");
check<sh::FieldPoint>("FieldPoint", "Game::FieldTemplate::Point");
check<sh::Crep>("Crep", "Game::CombatReport");
check<sh::CrepPrep>("CrepPrep", "Game::CombatPlayerReport");
check<sh::Srep>("Srep", "Game::CombatShipReport");

View file

@ -38,12 +38,6 @@ HOOK_RE = re.compile(r"^hook\.([^=]+)=")
EXEMPT = {"Game::Foo"}
EXEMPT_PREFIXES = ("Shim::SelfTest::",)
# Detours that are installed by default and are NOT template hooks. The FPU-force module's
# sampling keys accept exactly `on`/`off`. The M0 asm stub has no key at all and is always
# installed when `hooks != off`; it is mentioned in the OK line so nobody reads "27" as "all".
ALWAYS_ON_UNLESS_OFF = ("fpu.sample_turn", "fpu.sample_ticks")
UNNAMEABLE_DETOURS = ("Mars::Application::Initialize (M0 asm stub, always on unless hooks=off)",)
# Settings whose values legitimately end in a period or a path separator.
PROSE_SAFE = ("path", "out", "dir", "file")
@ -88,19 +82,6 @@ def check(cfg, hooks):
f"declares `# exhaustive` but does not name {len(missing)} registered hook(s), "
"which therefore default ON: " + ", ".join(missing)
)
# Template hooks are not the only detours. Lane CR named all 27, passed this check,
# and the shim installed SIX: the M0 Application::Initialize asm stub (unconditional
# whenever hooks != off -- it cannot be named off, so it is reported, not required)
# and the FPU-force module's four sampling detours, which are ON BY DEFAULT and are
# controlled by keys that do not start with `hook.`. An exhaustive config must turn
# those off explicitly, or "exhaustive" is a claim about the wrong list.
kv = {l.split("=", 1)[0].strip(): l.split("=", 1)[1].strip() for l in settings if "=" in l}
for key in ALWAYS_ON_UNLESS_OFF:
if kv.get(key) != "off":
problems.append(
f"declares `# exhaustive` but `{key}` is not `off` (it defaults ON and installs "
"detours no `hook.` key names)"
)
return problems
@ -123,8 +104,7 @@ def main():
for f in failures:
print(" " + f)
return 1
print(f"check_shim_configs: OK ({len(hooks)} template hooks + {len(ALWAYS_ON_UNLESS_OFF)} default-on "
f"FPU sampling keys + {len(UNNAMEABLE_DETOURS)} unnameable detour; {exhaustive} template(s) declared exhaustive)")
print(f"check_shim_configs: OK ({len(hooks)} registered hooks, {exhaustive} template(s) declared exhaustive)")
return 0