338 lines
13 KiB
C++
338 lines
13 KiB
C++
#include "shim/hooks/tech_effect_fields.h"
|
|
|
|
#include <cstring>
|
|
#include <vector>
|
|
|
|
#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<const char*>(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<const char*>(p) + off, sizeof v);
|
|
return v;
|
|
}
|
|
float f32_at(const void* p, std::size_t off) {
|
|
float v = 0;
|
|
std::memcpy(&v, static_cast<const char*>(p) + off, sizeof v);
|
|
return v;
|
|
}
|
|
void* ptr_at(const void* p, std::size_t off) {
|
|
void* v = nullptr;
|
|
std::memcpy(&v, static_cast<const char*>(p) + off, sizeof v);
|
|
return v;
|
|
}
|
|
std::uint8_t u8_at(const void* p, std::size_t off) {
|
|
return static_cast<const std::uint8_t*>(p)[off];
|
|
}
|
|
void put_i32(void* p, std::size_t off, std::int32_t v) { std::memcpy(static_cast<char*>(p) + off, &v, sizeof v); }
|
|
void put_u32(void* p, std::size_t off, std::uint32_t v) { std::memcpy(static_cast<char*>(p) + off, &v, sizeof v); }
|
|
void put_f32(void* p, std::size_t off, float v) { std::memcpy(static_cast<char*>(p) + off, &v, sizeof v); }
|
|
void put_u8(void* p, std::size_t off, std::uint8_t v) { static_cast<std::uint8_t*>(p)[off] = v; }
|
|
void put_ptr(void* p, std::size_t off, void* v) { std::memcpy(static_cast<char*>(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<Tv> 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<Tv> 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<Tv> 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<Tv> 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<char*>(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<int>(fx::PlayerFlag::TradeAllowed)] = u8_at(ab, 0x03) != 0;
|
|
s.flags[static_cast<int>(fx::PlayerFlag::CommerceRaiding)] = u8_at(ab, 0x04) != 0;
|
|
s.flags[static_cast<int>(fx::PlayerFlag::ViewIntel)] = u8_at(ab, 0x05) != 0;
|
|
s.flags[static_cast<int>(fx::PlayerFlag::GravSynth)] = u8_at(ab, 0x06) != 0;
|
|
s.flags[static_cast<int>(fx::PlayerFlag::AdvancedSensors)] = u8_at(ab, 0x07) != 0;
|
|
s.flags[static_cast<int>(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<int>(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<int>(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<std::uint32_t>(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
|