#include "shim/hooks/tech_effect_fields.h" #include #include #include "generated/sots_addresses.h" namespace shim::hooks::techfx { using trace::Tv; namespace tv = trace::tv; namespace fx = sots::effects; namespace { namespace A = sots::addr; std::int32_t i32_at(const void* p, std::size_t off) { std::int32_t v = 0; std::memcpy(&v, static_cast(p) + off, sizeof v); return v; } std::uint32_t u32_at(const void* p, std::size_t off) { std::uint32_t v = 0; std::memcpy(&v, static_cast(p) + off, sizeof v); return v; } float f32_at(const void* p, std::size_t off) { float v = 0; std::memcpy(&v, static_cast(p) + off, sizeof v); return v; } void* ptr_at(const void* p, std::size_t off) { void* v = nullptr; std::memcpy(&v, static_cast(p) + off, sizeof v); return v; } std::uint8_t u8_at(const void* p, std::size_t off) { return static_cast(p)[off]; } void put_i32(void* p, std::size_t off, std::int32_t v) { std::memcpy(static_cast(p) + off, &v, sizeof v); } void put_u32(void* p, std::size_t off, std::uint32_t v) { std::memcpy(static_cast(p) + off, &v, sizeof v); } void put_f32(void* p, std::size_t off, float v) { std::memcpy(static_cast(p) + off, &v, sizeof v); } void put_u8(void* p, std::size_t off, std::uint8_t v) { static_cast(p)[off] = v; } void put_ptr(void* p, std::size_t off, void* v) { std::memcpy(static_cast(p) + off, &v, sizeof v); } // One describer per field group, so a divergence names the field. Tv describe_suit(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("suit_tol", tv::f32(f32_at(p, 0))); s.add("max_overharvest", tv::f32(f32_at(p, 4))); return s; } Tv describe_res_mod(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("res_mod", tv::f32(f32_at(p, 0))); return s; } Tv describe_abilities(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("reb_ai", tv::boolean(u8_at(p, 0x00) != 0)); s.add("unk_fd", tv::u8(u8_at(p, 0x01))); s.add("ai_benefit", tv::boolean(u8_at(p, 0x02) != 0)); s.add("trade_allowed", tv::boolean(u8_at(p, 0x03) != 0)); s.add("commerce_raiding", tv::boolean(u8_at(p, 0x04) != 0)); s.add("view_intel", tv::boolean(u8_at(p, 0x05) != 0)); s.add("grav_synth", tv::boolean(u8_at(p, 0x06) != 0)); s.add("advanced_sensors", tv::boolean(u8_at(p, 0x07) != 0)); s.add("arcology", tv::boolean(u8_at(p, 0x08) != 0)); s.add("unk_105", tv::u8(u8_at(p, 0x09))); s.add("unk_106", tv::u8(u8_at(p, 0x0a))); s.add("unk_107", tv::u8(u8_at(p, 0x0b))); return s; } // +0x108 .. +0x15c. The five words this module does not model are emitted as raw u32 so a // surprise write by the original shows up as a real divergence rather than going unseen. Tv describe_modifiers(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("pddm", tv::f32(f32_at(p, 0x00))); std::vector con; for (int i = 0; i < 3; ++i) con.push_back(tv::f32(f32_at(p, 0x04 + 4u * i))); s.add("con_mod", tv::list(std::move(con))); std::vector sav; for (int i = 0; i < 3; ++i) sav.push_back(tv::f32(f32_at(p, 0x10 + 4u * i))); s.add("sav_mod", tv::list(std::move(sav))); s.add("out_mod", tv::f32(f32_at(p, 0x1c))); s.add("unk_128", tv::u32(u32_at(p, 0x20))); s.add("unk_12c", tv::u32(u32_at(p, 0x24))); s.add("pop_mod", tv::f32(f32_at(p, 0x28))); s.add("terra_mod", tv::f32(f32_at(p, 0x2c))); s.add("asteroid_mining", tv::boolean(u8_at(p, 0x30) != 0)); s.add("unk_139_13b", tv::u32(u32_at(p, 0x30) >> 8)); s.add("unk_13c", tv::u32(u32_at(p, 0x34))); s.add("min_rate", tv::f32(f32_at(p, 0x38))); s.add("unk_144", tv::u32(u32_at(p, 0x3c))); s.add("per_gate_traffic", tv::i32(i32_at(p, 0x40))); // an int, not a float s.add("unk_14c", tv::u32(u32_at(p, 0x44))); s.add("cast_range", tv::f32(f32_at(p, 0x48))); s.add("cast_efficiency", tv::f32(f32_at(p, 0x4c))); s.add("cast_threshold", tv::f32(f32_at(p, 0x50))); return s; } Tv describe_masks(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("design_options_a", tv::u32(u32_at(p, 0))); s.add("design_options_b", tv::u32(u32_at(p, 4))); return s; } Tv describe_translation(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("translation_known", tv::u32(u32_at(p, 0))); return s; } Tv describe_vaccines(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("has_vaccine", tv::u32(u32_at(p, 0))); s.add("has_immunity", tv::u32(u32_at(p, 4))); s.add("node_track_mask", tv::u32(u32_at(p, 8))); return s; } Tv describe_research_target(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); // A heap pointer, so only "still set" / "cleared" is meaningful across two runs. s.add("research_target", tv::ptr(ptr_at(p, 0))); s.add("research_target_set", tv::boolean(ptr_at(p, 0) != nullptr)); return s; } Tv describe_nodebore_ptr(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); // Pointer values are ignored by the default policy; `present` is the fact that matters, // and it is deliberately NOT compared (see the header): `ours` cannot allocate the // block, so a first bore-drive completion would diverge here for the wrong reason. s.add("node_bore_block", tv::ptr(ptr_at(p, 0))); return s; } Tv describe_inc_mod(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("inc_mod", tv::f32(f32_at(p, 0))); return s; } Tv describe_capture(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("capture_designs", tv::boolean(u8_at(p, 0) != 0)); s.add("unk_331_333", tv::u32(u32_at(p, 0) >> 8)); return s; } Tv describe_species_flags(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); std::vector f; for (int i = 0; i < sots::sim::kSpeciesCount; ++i) f.push_back(tv::u32(u32_at(p, 4u * i))); s.add("species_flags", tv::list(std::move(f))); s.add("count", tv::u32(u32_at(p, 0x1c))); return s; } Tv describe_roll(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); s.add("research_roll_pending", tv::boolean(u8_at(p, 0) != 0)); s.add("unk_3b5_3b7", tv::u32(u32_at(p, 0) >> 8)); s.add("ai_rebellion", tv::ptr(ptr_at(p, 4))); return s; } Tv describe_nodebore(const void* p, std::size_t, unsigned) { Tv s = tv::struct_(); std::vector v; for (int i = 0; i < 3; ++i) v.push_back(tv::i32(i32_at(p, 4u * i))); s.add("node_bore_params", tv::list(std::move(v))); return s; } } // namespace const RegionDef kRegions[kRegionCount] = { {"suit", A::ServerPlayer_off_SuitTol, 8, &describe_suit}, {"res_mod", A::ServerPlayer_off_ResMod, 4, &describe_res_mod}, {"abilities", A::ServerPlayer_off_RebAI, 12, &describe_abilities}, {"modifiers", A::ServerPlayer_off_pddm, 0x54, &describe_modifiers}, {"design_masks", A::ServerPlayer_off_TechMaskA, 8, &describe_masks}, {"translation", A::ServerPlayer_off_TranslationKnown, 4, &describe_translation}, {"vaccines", A::ServerPlayer_off_HasVac, 12, &describe_vaccines}, {"research_target", A::ServerPlayer_off_ResearchTarget, 4, &describe_research_target}, {"node_bore_ptr", A::ServerPlayer_off_NodeBore, 4, &describe_nodebore_ptr}, {"inc_mod", A::ServerPlayer_off_IncMod, 4, &describe_inc_mod}, {"capture_designs", A::ServerPlayer_off_CaptureDesigns, 4, &describe_capture}, {"species_flags", A::ServerPlayer_off_SpeciesTechFlags, 0x20, &describe_species_flags}, {"roll", A::ServerPlayer_off_ResearchRollPending, 8, &describe_roll}, {"node_bore", 0, 12, &describe_nodebore}, // base is *(player + off_NodeBore) }; std::size_t PlayerSpan() { return A::ServerPlayer_off_ResearchRollPending + 8; } Views Views::OverPlayer(void* player) { Views v; for (int i = 0; i < kRegionCount; ++i) { v.base[i] = i == R_NODEBORE ? ptr_at(player, A::ServerPlayer_off_NodeBore) : static_cast(player) + kRegions[i].off; } return v; } // Seed our model from the pre-call field values. Everything the callback can write comes // from the region copies; species (never written) comes from the live object. fx::PlayerEconomyState ReadPlayerState(const Views& v_, sots::sim::Species species) { fx::PlayerEconomyState s; s.species = species; const void* suit = v_.base[R_SUIT]; s.suitTol = f32_at(suit, 0); s.maxOverharvest = f32_at(suit, 4); s.resMod = f32_at(v_.base[R_RESMOD], 0); s.incMod = f32_at(v_.base[R_INCMOD], 0); const void* ab = v_.base[R_ABILITIES]; s.rebelAI = u8_at(ab, 0x00) != 0; s.aiBenefit = u8_at(ab, 0x02) != 0; s.flags[static_cast(fx::PlayerFlag::TradeAllowed)] = u8_at(ab, 0x03) != 0; s.flags[static_cast(fx::PlayerFlag::CommerceRaiding)] = u8_at(ab, 0x04) != 0; s.flags[static_cast(fx::PlayerFlag::ViewIntel)] = u8_at(ab, 0x05) != 0; s.flags[static_cast(fx::PlayerFlag::GravSynth)] = u8_at(ab, 0x06) != 0; s.flags[static_cast(fx::PlayerFlag::AdvancedSensors)] = u8_at(ab, 0x07) != 0; s.flags[static_cast(fx::PlayerFlag::Arcology)] = u8_at(ab, 0x08) != 0; const void* m = v_.base[R_MODIFIERS]; s.defenceDamageMod = f32_at(m, 0x00); for (int i = 0; i < 3; ++i) s.conMod[i] = f32_at(m, 0x04 + 4u * i); for (int i = 0; i < 3; ++i) s.savMod[i] = f32_at(m, 0x10 + 4u * i); s.outMod = f32_at(m, 0x1c); s.popMod = f32_at(m, 0x28); s.terraMod = f32_at(m, 0x2c); s.flags[static_cast(fx::PlayerFlag::AsteroidMining)] = u8_at(m, 0x30) != 0; s.miningRate = f32_at(m, 0x38); s.perGateTraffic = i32_at(m, 0x40); s.castRange = f32_at(m, 0x48); s.castEfficiency = f32_at(m, 0x4c); s.castThreshold = f32_at(m, 0x50); const void* v = v_.base[R_VACCINES]; s.hasVaccine = u32_at(v, 0); s.hasImmunity = u32_at(v, 4); s.nodeTrackMask = u32_at(v, 8); s.translationKnownMask = u32_at(v_.base[R_TRANSLATION], 0); s.flags[static_cast(fx::PlayerFlag::CaptureDesigns)] = u8_at(v_.base[R_CAPTURE], 0) != 0; // Present exactly when the block exists: in compare mode that is the declared region, // in replace mode the live pointer. if (const void* nb = v_.base[R_NODEBORE]) { s.hasNodeBoreParams = true; for (int i = 0; i < 3; ++i) s.nodeBoreParams[i] = i32_at(nb, 4u * i); } return s; } void WritePlayerState(const Views& v_, const fx::PlayerEconomyState& s) { void* suit = v_.base[R_SUIT]; put_f32(suit, 0, s.suitTol); put_f32(suit, 4, s.maxOverharvest); put_f32(v_.base[R_RESMOD], 0, s.resMod); put_f32(v_.base[R_INCMOD], 0, s.incMod); void* ab = v_.base[R_ABILITIES]; put_u8(ab, 0x02, s.aiBenefit ? 1 : 0); put_u8(ab, 0x03, s.Flag(fx::PlayerFlag::TradeAllowed) ? 1 : 0); put_u8(ab, 0x04, s.Flag(fx::PlayerFlag::CommerceRaiding) ? 1 : 0); put_u8(ab, 0x05, s.Flag(fx::PlayerFlag::ViewIntel) ? 1 : 0); put_u8(ab, 0x06, s.Flag(fx::PlayerFlag::GravSynth) ? 1 : 0); put_u8(ab, 0x07, s.Flag(fx::PlayerFlag::AdvancedSensors) ? 1 : 0); put_u8(ab, 0x08, s.Flag(fx::PlayerFlag::Arcology) ? 1 : 0); void* m = v_.base[R_MODIFIERS]; put_f32(m, 0x00, s.defenceDamageMod); for (int i = 0; i < 3; ++i) put_f32(m, 0x04 + 4u * i, s.conMod[i]); for (int i = 0; i < 3; ++i) put_f32(m, 0x10 + 4u * i, s.savMod[i]); put_f32(m, 0x1c, s.outMod); put_f32(m, 0x28, s.popMod); put_f32(m, 0x2c, s.terraMod); put_u8(m, 0x30, s.Flag(fx::PlayerFlag::AsteroidMining) ? 1 : 0); put_f32(m, 0x38, s.miningRate); put_i32(m, 0x40, s.perGateTraffic); put_f32(m, 0x48, s.castRange); put_f32(m, 0x4c, s.castEfficiency); put_f32(m, 0x50, s.castThreshold); void* v = v_.base[R_VACCINES]; put_u32(v, 0, s.hasVaccine); put_u32(v, 4, s.hasImmunity); put_u32(v, 8, s.nodeTrackMask); put_u32(v_.base[R_TRANSLATION], 0, s.translationKnownMask); put_u8(v_.base[R_CAPTURE], 0, s.Flag(fx::PlayerFlag::CaptureDesigns) ? 1 : 0); void* sf = v_.base[R_SPECIES_FLAGS]; for (int i = 0; i < sots::sim::kSpeciesCount; ++i) put_u32(sf, 4u * i, s.speciesFlags[i]); put_u32(sf, 0x1c, static_cast(sots::sim::kSpeciesCount)); // The block is only writable where it already exists; see the header. void* nb = v_.base[R_NODEBORE]; if (nb && s.hasNodeBoreParams) { for (int i = 0; i < 3; ++i) put_i32(nb, 4u * i, s.nodeBoreParams[i]); } } bool ClearResearchTargetIfMatched(const Views& v_, const void* def) { void* rt = v_.base[R_RESEARCH_TARGET]; void* roll = v_.base[R_ROLL]; if (!rt || ptr_at(rt, 0) != def) return false; const bool pending = roll && u8_at(roll, 0) != 0; if (roll) put_u8(roll, 0, 0); put_ptr(rt, 0, nullptr); // a pointer-sized zero: 4 bytes on the i386 target return pending; } void WriteDesignOptionMasks(const Views& v_, std::uint32_t a, std::uint32_t b) { void* mk = v_.base[R_MASKS]; if (!mk) return; put_u32(mk, 0, a); put_u32(mk, 4, b); } } // namespace shim::hooks::techfx