// Hook descriptor for the hard-coded tech-effect callback (B2): // // Game::ServerPlayer::OnTechResearched(this, TechDef* def, bool silent) // // A verified __thiscall in sots_addresses.h (vft slot 4), so it goes through Hook<> with // CallConv::Thiscall. TechTree::SetResearched calls it every time a tech is marked // researched -- from the per-turn research pass (B3's hook), from the Zuul boarding-pod // grant this very function makes, and from scenario grants. // // What it does, and therefore what this hook has to reproduce: an if/else-if chain over // tech ids 10000..10031 (at most one branch runs) that writes the player's economy // modifiers, followed by a tail that runs on *every* completion -- the plague-cure mask, // the two design-option masks, the node-bore parameters, the per-species xenotech flag // words and their sticky translation mask, the Zuul boarding-pod grant, the // capture-designs pair test, and the temperance sweep. // // Region model: one region per field group of the player, each with a struct describer, // so a divergence names the field (`side.modifiers.after.v.out_mod`) instead of a byte // offset in one opaque blob. The regions do not cover the whole object, so `ours` reads // every field the call can modify out of the scratch copies and everything else (species, // RebAI, the tech tree) from the live player, where "live" and "before" are the same. // // What `ours` deliberately does NOT do, because compare mode must not touch live game // state or consume randomness: // * the completion events (EVENT_RESEARCH_COMPLETE / _UNDERBUDGET / _TEMPERANCE); // * the pending plague-cure roll (it would draw from the real generator) -- the two // words it guards are still cleared, and the record says whether it would have fired; // * the writes to *other* objects: the owned systems' AI flag, the arcology civilian-cap // re-evaluation, the addiction cure, and the plague clear on systems and ships; // * TechTree::SetResearched for the Zuul boarding pods (it would mutate the tree). // Each of those is reported in the record's `ours` fields instead, so a trace still shows // what our layer decided even where the compare cannot check it. #pragma once #include #include #include #include "shim/trace/hook.h" namespace shim::hooks { struct ServerPlayerOnTechResearchedHook { static constexpr const char* name = "Game::ServerPlayer::OnTechResearched"; static constexpr trace::CallConv conv = trace::CallConv::Thiscall; using Ret = void; // this (ServerPlayer*), def (TechDef*), silent using Args = std::tuple; static void describe_args(std::vector& out, void* self, void* def, bool silent); static void regions(std::vector& out, void* self, void* def, bool silent); static Args rebind(trace::Scratch& s, void* self, void* def, bool silent); static void ours(void* self, void* def, bool silent); static trace::HookPolicy policy() { return trace::HookPolicy{}; } }; // Process facts the hook needs (exe base for the RVAs, a line logger). Call once before // installing. void init_tech_effects(std::uintptr_t exe_base, void (*log_line)(const char* line)); } // namespace shim::hooks