// The byte-level adapter between a live `ServerPlayer` and game/effects' PlayerEconomyState, // plus the region table and struct describers the B2 hook declares. // // Split out of the hook (as B1 split `budget_inputs`) so that every offset, every float32 // round-trip and every field the tech-effect callback writes can be exercised on the host // against a synthetic player buffer, without the game and without a VM window. The hook // itself then only has to resolve the tech id and delegate to the game's tech tree. // // The regions do not cover the whole object: they are exactly the field groups the callback // writes, one region each, so a divergence names the field. `Views` is the indirection that // lets the same code run against the scratch copies (compare mode) and against the live // object (replace mode). #pragma once #include #include #include "game/effects/tech_effects.h" #include "game/sim/species.h" #include "shim/trace/tracer.h" namespace shim::hooks::techfx { enum RegionId { R_SUIT = 0, // SuitTol, MaxOH R_RESMOD, // ResMod R_ABILITIES, // RebAI .. harcc R_MODIFIERS, // pddm .. CstT R_MASKS, // design-option masks A / B R_TRANSLATION, // sticky translation mask R_VACCINES, // HasVac, HasImm, NPTrk R_RESEARCH_TARGET, // ResT R_NODEBORE_PTR, // pointer to the node-bore block R_INCMOD, // IncMod R_CAPTURE, // cdp R_SPECIES_FLAGS, // flags[7] + count R_ROLL, // pending plague-cure roll + AIRebellion* R_NODEBORE, // the 3-word node-bore block itself (behind R_NODEBORE_PTR) R_RNG, // the strategic Mars::RNG object (behind player+8 -> server+0x16c) kRegionCount }; struct RegionDef { const char* name; std::uint32_t off; // from the ServerPlayer base; meaningless for R_NODEBORE std::size_t size; trace::Tv (*describe)(const void* data, std::size_t size, unsigned inline_max); }; extern const RegionDef kRegions[kRegionCount]; // The last byte the region table reaches, for one readability check up front. std::size_t PlayerSpan(); // Where each region's bytes live for this call. A null entry means "not present": only // R_NODEBORE is ever legitimately null (no bore drive researched yet). struct Views { void* base[kRegionCount] = {}; // The generator's live address, for the describer's next-pointer arithmetic: `next` is a // heap address, so it is reported as its index into mt, which is what it means and what // survives being rewritten by a reimplementation. void* liveRng = nullptr; // Point every region at its offset inside a live (or synthetic) player object, and // R_NODEBORE at whatever the block pointer holds. static Views OverPlayer(void* player); }; // Seed our model from the pre-call field values. `species` is read from the live object by // the caller because no region covers it (the callback never writes it). sots::effects::PlayerEconomyState ReadPlayerState(const Views& v, sots::sim::Species species); // Write back exactly the words the callback writes. The node-bore block is only written // where it already exists. void WritePlayerState(const Views& v, const sots::effects::PlayerEconomyState& s); // The callback's first act when the completing definition is the current research target: // clear the target and the pending-roll byte. Returns whether the roll was pending (the // roll itself draws randomness, so a differential run reports it instead of running it). bool ClearResearchTargetIfMatched(const Views& v, const void* def); // The two design-option masks, written straight into R_MASKS. void WriteDesignOptionMasks(const Views& v, std::uint32_t a, std::uint32_t b); // The describer needs the live generator address; set it before a snapshot is described. void SetLiveRngBase(const void* p); } // namespace shim::hooks::techfx