#include "game/effects/tech_effects.h" #include "game/effects/tech_id.h" #include #include #include "check.h" // shared with tests/game_sim using namespace sots::effects; using sots::sim::Species; static ApplyContext ctx_with_tuning(sots::sim::TuningTable& t) { ApplyContext c; c.tuning = &t; return c; } static void test_ids() { CHECK_EQ(static_cast(TechId::CCC_AdvSens), 10000); CHECK_EQ(static_cast(TechId::IND_HrdStrct), 10030); CHECK_EQ(static_cast(TechId::WEP_ACCAMP), 10192); CHECK_EQ(static_cast(TechId::None), 10197); CHECK(IsValidTechId(10000) && IsValidTechId(10195)); CHECK(!IsValidTechId(9999) && !IsValidTechId(10196) && !IsValidTechId(TechId::None)); CHECK_EQ(TechIdIndex(TechId::DRV_RAD), 66); CHECK(TechIdFromIndex(64) == TechId::DRV_RIP); CHECK(std::string(TechIdName(TechId::IND_Waldo)) == "IND_Waldo"); CHECK(std::string(TechIdName(TechId::DRV_FUSN)) == "DRV_FUSN"); CHECK(TechIdName(TechId::None) == nullptr); // The table is complete: every position has a name, and every name resolves back. for (int i = 0; i < kTechIdCount; ++i) { const char* n = TechIdName(TechIdFromIndex(i)); CHECK(n != nullptr && n[0] != '\0'); } CHECK(TechIdFromName("IND_Waldo") == TechId::IND_Waldo); CHECK(TechIdFromName("ind_waldo") == TechId::IND_Waldo); // case-insensitive like the game CHECK(TechIdFromName("CCC_TRNSLIR") == TechId::XNC_Translation1_Liir); CHECK(TechIdFromName("not_a_tech") == TechId::None); CHECK(TechIdFromName("") == TechId::None); // every resolved name round-trips, and the table has exactly 196 slots int named = 0; for (int i = 0; i < kTechIdCount; ++i) { const char* n = TechIdName(TechIdFromIndex(i)); if (n == nullptr) continue; ++named; CHECK(TechIdFromName(n) == TechIdFromIndex(i)); } CHECK_EQ(named, kTechIdCount); // the whole table is recovered // xenotech blocks CHECK(XenoTechId(XenoLevel::Translation1, Species::Human) == TechId::XNC_Translation1_Human); CHECK(XenoTechId(XenoLevel::Translation1, Species::Morrigi) == TechId::XNC_Translation1_Morrigi); CHECK_EQ(static_cast(XenoTechId(XenoLevel::Translation2, Species::Morrigi)), 10125); CHECK_EQ(static_cast(XenoTechId(XenoLevel::Incorporate, Species::Morrigi)), 10136); CHECK(XenoTechId(XenoLevel::Incorporate, Species::Zuul) == TechId::None); CHECK(XenoTechId(XenoLevel::Subjugate, Species::Zuul) == TechId::XNC_Subjugate_Zuul); CHECK_EQ(static_cast(XenoTechId(XenoLevel::Proliferate, Species::Morrigi)), 10162); CHECK(XenoTechId(XenoLevel::Temperance, Species::NPC) == TechId::None); CHECK(XenoTechId(XenoLevel::Translation3, Species::NPC) == TechId::None); CHECK(std::string(NodeTrackTechName(Species::Human)) == "CCC_NDTRKHUM"); CHECK(std::string(NodeTrackTechName(Species::Zuul)) == "CCC_NDTRKZUL"); CHECK(NodeTrackTechName(Species::Hiver) == nullptr); } static void test_industrial() { PlayerEconomyState s; ApplyContext ctx; TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_Waldo, ctx); CHECK(o.applied); CHECK_NEAR(s.conMod[0], 0.90, 1e-6); CHECK_NEAR(s.conMod[1], 0.90, 1e-6); CHECK_NEAR(s.conMod[2], 0.90, 1e-6); CHECK_NEAR(s.outMod, 1.15, 1e-6); CHECK(s.HasResearched(TechId::IND_Waldo)); o = ApplyTechEffect(s, TechId::IND_Waldo, ctx); // no double application CHECK(!o.applied); CHECK_NEAR(s.outMod, 1.15, 1e-6); ApplyTechEffect(s, TechId::IND_CyberInt, ctx); // -0.05 / +0.20 CHECK_NEAR(s.conMod[0], 0.85, 1e-6); CHECK_NEAR(s.outMod, 1.35, 1e-6); ApplyTechEffect(s, TechId::IND_ExpSys, ctx); // -0.10 / +0.15 CHECK_NEAR(s.conMod[2], 0.75, 1e-6); CHECK_NEAR(s.outMod, 1.50, 1e-6); ApplyTechEffect(s, TechId::IND_OrbDry, ctx); // classes 1 and 2 only CHECK_NEAR(s.conMod[0], 0.75, 1e-6); CHECK_NEAR(s.conMod[1], 0.70, 1e-6); CHECK_NEAR(s.conMod[2], 0.70, 1e-6); ApplyTechEffect(s, TechId::DRN_AdvRob, ctx); CHECK_NEAR(s.conMod[0], 0.70, 1e-6); CHECK_NEAR(s.conMod[1], 0.65, 1e-6); ApplyTechEffect(s, TechId::IND_OrbFound, ctx); CHECK_NEAR(s.savMod[0], 0.95, 1e-6); CHECK_NEAR(s.savMod[2], 0.95, 1e-6); ApplyTechEffect(s, TechId::IND_GravCon, ctx); // 1.80 ApplyTechEffect(s, TechId::IND_HvyPlat, ctx); // 1.90 CHECK_NEAR(s.outMod, 1.90, 1e-6); ApplyTechEffect(s, TechId::IND_HrdStrct, ctx); // multiplicative CHECK_NEAR(s.outMod, 1.71, 1e-6); CHECK_NEAR(s.defenceDamageMod, 0.25, 1e-6); // the order of additive and multiplicative effects matters PlayerEconomyState r; ApplyTechEffect(r, TechId::IND_HrdStrct, ctx); ApplyTechEffect(r, TechId::IND_GravCon, ctx); CHECK_NEAR(r.outMod, 1.20, 1e-6); // 0.9 + 0.3, not 1.3 x 0.9 ApplyTechEffect(s, TechId::IND_AstMine, ctx); CHECK(s.Flag(PlayerFlag::AsteroidMining)); ApplyTechEffect(s, TechId::IND_MsMine, ctx); CHECK_NEAR(s.maxOverharvest, 0.1f, 0.0); CHECK_NEAR(s.miningRate, 1.0, 0.0); PlayerEconomyState q; q.maxOverharvest = 0.3f; ApplyTechEffect(q, TechId::IND_MsMine, ctx); CHECK_NEAR(q.maxOverharvest, 0.3f, 0.0); // max(), not assignment ApplyTechEffect(s, TechId::CCC_AdvSens, ctx); CHECK(s.Flag(PlayerFlag::AdvancedSensors)); } static void test_biology() { PlayerEconomyState s; s.suitTol = 0.07; ApplyContext ctx; ApplyTechEffect(s, TechId::BIO_GnMod, ctx); CHECK_NEAR(s.popMod, 1.10, 1e-6); ApplyTechEffect(s, TechId::BIO_AtmoAd, ctx); CHECK_NEAR(s.suitTol, 0.82, 1e-6); CHECK_NEAR(s.popMod, 1.16, 1e-6); CHECK_NEAR(s.terraMod, 1.35, 1e-6); ApplyTechEffect(s, TechId::BIO_GrvAdpt, ctx); CHECK_NEAR(s.suitTol, 2.32, 1e-6); CHECK_NEAR(s.popMod, 1.26, 1e-6); CHECK_NEAR(s.terraMod, 1.70, 1e-6); ApplyTechEffect(s, TechId::BIO_EnvTail, ctx); CHECK_NEAR(s.popMod, 1.46, 1e-6); CHECK_NEAR(s.terraMod, 2.15, 1e-6); ApplyTechEffect(s, TechId::IND_EleNans, ctx); // +0.60 ApplyTechEffect(s, TechId::BIO_TerBac, ctx); // +0.45 ApplyTechEffect(s, TechId::IND_AtProc, ctx); // +0.50 CHECK_NEAR(s.terraMod, 3.70, 1e-6); TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_ArcCon, ctx); CHECK(s.Flag(PlayerFlag::Arcology)); CHECK_NEAR(s.popMod, 1.61, 1e-6); CHECK(o.reevaluateCivilianCaps); } static void test_gates_and_casting() { sots::sim::TuningTable t; t.PERGATETRAFFIC_DRV_TpGate = 5; t.PERGATETRAFFIC_DRV_GatAmp = 12; ApplyContext ctx = ctx_with_tuning(t); PlayerEconomyState s; ApplyTechEffect(s, TechId::DRV_TpGate, ctx); CHECK_NEAR(s.perGateTraffic, 5.0, 0.0); ApplyTechEffect(s, TechId::DRV_GatAmp, ctx); CHECK_NEAR(s.perGateTraffic, 12.0, 0.0); PlayerEconomyState r; // reverse order: max wins ApplyTechEffect(r, TechId::DRV_GatAmp, ctx); ApplyTechEffect(r, TechId::DRV_TpGate, ctx); CHECK_NEAR(r.perGateTraffic, 12.0, 0.0); PlayerEconomyState n; // no tuning: nothing to raise ApplyTechEffect(n, TechId::DRV_TpGate, ApplyContext{}); CHECK_NEAR(n.perGateTraffic, 0.0, 0.0); ApplyTechEffect(s, TechId::DRV_FarCast, ctx); CHECK_NEAR(s.castRange, 10.0, 0.0); CHECK_NEAR(s.castEfficiency, 2.0, 0.0); CHECK_NEAR(s.castThreshold, 1.0, 0.0); ApplyTechEffect(s, TechId::DRV_GrvSyn, ctx); CHECK(s.Flag(PlayerFlag::GravSynth)); } static void test_ai() { ApplyContext ctx; ctx.aiBonus = {0.2, 0.3, 0.4}; PlayerEconomyState s; CHECK(s.aiBenefit); ApplyTechEffect(s, TechId::CCC_AI, ctx); CHECK_NEAR(s.resMod, 1.2, 1e-6); ApplyTechEffect(s, TechId::CCC_AIAdmin, ctx); CHECK_NEAR(s.incMod, 1.3, 1e-6); ApplyTechEffect(s, TechId::CCC_AIFac, ctx); CHECK_NEAR(s.outMod, 1.4, 1e-6); SetAiBenefit(s, false, ctx); // rebellion: bonuses withdrawn CHECK_NEAR(s.resMod, 1.0, 1e-6); CHECK_NEAR(s.incMod, 1.0, 1e-6); CHECK_NEAR(s.outMod, 1.0, 1e-6); SetAiBenefit(s, false, ctx); // idempotent CHECK_NEAR(s.outMod, 1.0, 1e-6); SetAiBenefit(s, true, ctx); CHECK_NEAR(s.resMod, 1.2, 1e-6); CHECK_NEAR(s.outMod, 1.4, 1e-6); PlayerEconomyState off; // researched while off: nothing off.aiBenefit = false; ApplyTechEffect(off, TechId::CCC_AIFac, ctx); CHECK_NEAR(off.outMod, 1.0, 1e-6); TechApplyOutcome o = ApplyTechEffect(off, TechId::CCC_AISlv, ctx); // slave AI: benefit back on CHECK(off.aiBenefit); CHECK_NEAR(off.outMod, 1.4, 1e-6); CHECK(o.flagSystemsAI); CHECK(!AiRebellionPossible(off)); PlayerEconomyState v; CHECK(AiRebellionPossible(v)); o = ApplyTechEffect(v, TechId::CCC_AIVrus, ctx); CHECK(o.flagSystemsAI); CHECK(AiRebellionPossible(v)); v.species = Species::NPC; CHECK(!AiRebellionPossible(v)); PlayerEconomyState none; // caller overrides to 0 ApplyContext zero; zero.aiBonus = AiBonusValues{0, 0, 0}; ApplyTechEffect(none, TechId::CCC_AI, zero); CHECK_NEAR(none.resMod, 1.0, 0.0); // The table's own values, recovered in B2: 0.5 into each of the three slots. PlayerEconomyState dflt; ApplyTechEffect(dflt, TechId::CCC_AI, ApplyContext{}); CHECK_NEAR(dflt.resMod, 1.5, 0.0); ApplyTechEffect(dflt, TechId::CCC_AIAdmin, ApplyContext{}); CHECK_NEAR(dflt.incMod, 1.5, 0.0); ApplyTechEffect(dflt, TechId::CCC_AIFac, ApplyContext{}); CHECK_NEAR(dflt.outMod, 1.5, 0.0); CHECK_NEAR(AiRebellionOdds(TechId::CCC_AI), 0.1f, 0.0); CHECK_NEAR(AiRebellionOdds(TechId::CCC_AIFRCON), 0.2f, 0.0); CHECK_NEAR(AiRebellionOdds(TechId::CCC_AISlv), 0.0, 0.0); } static void test_flags_and_species() { ApplyContext ctx; PlayerEconomyState s; ApplyTechEffect(s, TechId::CCC_FtlEcon, ctx); CHECK(s.Flag(PlayerFlag::TradeAllowed)); PlayerEconomyState rebel; rebel.rebelAI = true; ApplyTechEffect(rebel, TechId::CCC_FtlEcon, ctx); CHECK(!rebel.Flag(PlayerFlag::TradeAllowed)); ApplyTechEffect(s, TechId::CCC_ComRaid, ctx); CHECK(s.Flag(PlayerFlag::CommerceRaiding)); ApplyTechEffect(s, TechId::CCC_DatCor, ctx); CHECK(s.Flag(PlayerFlag::ViewIntel)); // design capture needs both techs, in either order ApplyTechEffect(s, TechId::CCC_SpyBm, ctx); CHECK(!s.Flag(PlayerFlag::CaptureDesigns)); ApplyTechEffect(s, TechId::IND_SlvgTech, ctx); CHECK(s.Flag(PlayerFlag::CaptureDesigns)); PlayerEconomyState r; ApplyTechEffect(r, TechId::IND_SlvgTech, ctx); CHECK(!r.Flag(PlayerFlag::CaptureDesigns)); ApplyTechEffect(r, TechId::CCC_SpyBm, ctx); CHECK(r.Flag(PlayerFlag::CaptureDesigns)); // Zuul get boarding pods with cruiser construction; nobody else does TechApplyOutcome o = ApplyTechEffect(s, TechId::IND_CruisCon, ctx); CHECK(o.grantedTech == TechId::None); PlayerEconomyState z; z.species = Species::Zuul; o = ApplyTechEffect(z, TechId::IND_CruisCon, ctx); CHECK(o.grantedTech == TechId::IND_BrdPod); CHECK(!z.HasResearched(TechId::IND_BrdPod)); // the caller researches it o = ApplyTechEffect(z, o.grantedTech, ctx); CHECK(o.applied && z.HasResearched(TechId::IND_BrdPod)); // node-bore parameters: highest drive wins regardless of order ApplyTechEffect(z, TechId::DRV_RIP, ctx); CHECK_EQ(z.nodeBoreParams[0], 45); CHECK_EQ(z.nodeBoreParams[2], 3); o = ApplyTechEffect(z, TechId::DRV_RAD, ctx); CHECK(o.nodeBoreParamsChanged); CHECK_EQ(z.nodeBoreParams[0], 95); CHECK_EQ(z.nodeBoreParams[1], 60); o = ApplyTechEffect(z, TechId::DRV_REND, ctx); CHECK(!o.nodeBoreParamsChanged); CHECK_EQ(z.nodeBoreParams[0], 95); } static void test_plague() { ApplyContext ctx; PlayerEconomyState s; CHECK_EQ(PlagueCureMask(TechId::BIO_PLGVAC), 1u); CHECK_EQ(PlagueCureMask(TechId::BIO_CONNAN), 16u); CHECK_EQ(PlagueCureMask(TechId::BIO_UNIANTI), 15u); CHECK_EQ(PlagueCureMask(TechId::IND_Waldo), 0u); TechApplyOutcome o = ApplyTechEffect(s, TechId::BIO_RTPLGVAC, ctx); CHECK_EQ(s.hasVaccine, 2u); CHECK_EQ(s.hasImmunity, 2u); CHECK_EQ(o.plagueCuredMask, 2u); o = ApplyTechEffect(s, TechId::BIO_UNIANTI, ctx); CHECK_EQ(s.hasVaccine, 15u); CHECK_EQ(o.plagueCuredMask, 15u); ApplyTechEffect(s, TechId::BIO_CONNAN, ctx); CHECK_EQ(s.hasImmunity, 31u); } static void test_xenotech() { ApplyContext ctx; PlayerEconomyState s; TechApplyOutcome o = ApplyTechEffect(s, TechId::XNC_Translation1_Liir, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Liir)], 0x001u); CHECK_EQ(s.speciesFlags[static_cast(Species::Human)], 0u); CHECK_EQ(o.temperanceSpeciesMask, 0u); ApplyTechEffect(s, TechId::XNC_Translation3_Liir, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Liir)], 0x005u); o = ApplyTechEffect(s, TechId::XNC_Temperance_Hiver, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Hiver)], 0x020u); CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast(Species::Hiver)); o = ApplyTechEffect(s, TechId::XNC_Accommodate_Morrigi, ctx); // every completion reports temperance CHECK_EQ(s.speciesFlags[static_cast(Species::Morrigi)], 0x080u); CHECK_EQ(o.temperanceSpeciesMask, 1u << static_cast(Species::Hiver)); // Proliferate has no Zuul entry, so the Zuul word never gets bit 8. ApplyTechEffect(s, TechId::XNC_Proliferate_Tarkas, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Tarkas)], 0x100u); ApplyTechEffect(s, TechId::XNC_Subjugate_Zuul, ctx); CHECK_EQ(s.speciesFlags[static_cast(Species::Zuul)], 0x040u); // rebuild from the researched set alone (load path) PlayerEconomyState l; l.researched = s.researched; RebuildSpeciesTechFlags(l); for (int sp = 0; sp < sots::sim::kSpeciesCount; ++sp) CHECK_EQ(l.speciesFlags[sp], s.speciesFlags[sp]); } static void test_by_name() { ApplyContext ctx; PlayerEconomyState s; TechApplyOutcome o = ApplyTechEffectByName(s, "ind_gravcon", ctx); CHECK(o.applied); CHECK_NEAR(s.outMod, 1.30, 1e-6); o = ApplyTechEffectByName(s, "CCC_NDTRKHUM", ctx); CHECK(o.applied); CHECK_EQ(s.nodeTrackMask, 1u << static_cast(Species::Human)); o = ApplyTechEffectByName(s, "ccc_ndtrkzul", ctx); CHECK_EQ(s.nodeTrackMask, (1u << static_cast(Species::Human)) | (1u << static_cast(Species::Zuul))); o = ApplyTechEffectByName(s, "WEP_SOMETHING_DATA_ONLY", ctx); CHECK(!o.applied); CHECK_NEAR(s.outMod, 1.30, 1e-6); o = ApplyTechEffectByName(s, "IND_SPNLMNT", ctx); // in the table, no effect CHECK(o.applied); CHECK(s.HasResearched(TechId::IND_SPNLMNT)); CHECK_NEAR(s.outMod, 1.30, 1e-6); } static void test_table_shape() { // Techs with a strategic effect have entries; unresolved and gate-only ids do not. CHECK(!EffectsOf(TechId::IND_Waldo).empty()); CHECK_EQ(EffectsOf(TechId::IND_Waldo).size(), std::size_t{2}); CHECK(EffectsOf(TechId::DRV_FUSN).empty()); CHECK(EffectsOf(TechId::CCC_HYPCOM).empty()); CHECK(EffectsOf(TechId::None).empty()); int withEffects = 0; for (int i = 0; i < kTechIdCount; ++i) { if (!EffectsOf(TechIdFromIndex(i)).empty()) ++withEffects; } CHECK_EQ(withEffects, 44); // The proliferate block is five entries and the two slots after it are the node-track // techs -- the earlier reconstruction had a six-entry block running over them. CHECK(XenoTechId(XenoLevel::Proliferate, Species::Zuul) == TechId::None); CHECK(XenoTechId(XenoLevel::Proliferate, Species::Morrigi) == TechId::XNC_Proliferate_Morrigi); CHECK_EQ(static_cast(TechId::XNC_Proliferate_Morrigi), 10162); CHECK_EQ(static_cast(TechId::CCC_NDTRKHUM), 10163); CHECK_EQ(static_cast(TechId::CCC_NDTRKZUL), 10164); CHECK(TechIdFromName(NodeTrackTechName(Species::Human)) == TechId::CCC_NDTRKHUM); CHECK(TechIdFromName(NodeTrackTechName(Species::Zuul)) == TechId::CCC_NDTRKZUL); CHECK(NodeTrackTechName(Species::Morrigi) == nullptr); // design-option masks std::set have = {"IND_REFCOAT", "SLD_INTANG", "DRV_NODE", "WEP_HvyPmsl"}; DesignOptionMasks m = ComputeDesignOptionMasks([&](std::string_view n) { return have.count(std::string(n)) > 0; }); CHECK_EQ(m.a, (1u << 0) | (1u << 15)); CHECK_EQ(m.b, (1u << 0) | (1u << 28)); m = ComputeDesignOptionMasks([](std::string_view) { return false; }); CHECK_EQ(m.a, 0u); CHECK_EQ(m.b, 0u); m = ComputeDesignOptionMasks([](std::string_view) { return true; }); CHECK_EQ(m.a, 0xffffffffu); CHECK_EQ(m.b, 0x1fffffffu); } // ---- B2: the corrections read off the completion callback's instruction stream -------- static void test_float32_state() { ApplyContext ctx; // The modifiers are float32 in the player object and every step rounds through float32. // Six terraform techs in a row: accumulating in double and rounding once gives a // different float from rounding at every step, which is what the original does. PlayerEconomyState s; const TechId chain[] = {TechId::BIO_AtmoAd, TechId::BIO_GrvAdpt, TechId::BIO_EnvTail, TechId::BIO_TerBac, TechId::IND_AtProc, TechId::IND_EleNans}; for (TechId id : chain) ApplyTechEffect(s, id, ctx); float stepwise = 1.f; for (double k : {0.35f, 0.35f, 0.45f, 0.45f, 0.50f, 0.60f}) stepwise = static_cast(static_cast(stepwise) + k); CHECK(s.terraMod == stepwise); // The constants are widened float32 literals, not the exact decimals they look like. CHECK(static_cast(0.05f) != 0.05); CHECK(static_cast(0.45f) != 0.45); CHECK(static_cast(0.9f) != 0.9); CHECK(static_cast(0.75f) == 0.75); // exactly representable, unaffected // The multiplicative tech runs on the float32 that the additive ones left behind. PlayerEconomyState m; ApplyTechEffect(m, TechId::IND_Waldo, ctx); ApplyTechEffect(m, TechId::IND_HrdStrct, ctx); const float expect = static_cast( static_cast(static_cast(1.0 + static_cast(0.15f))) * static_cast(0.9f)); CHECK(m.outMod == expect); CHECK(m.defenceDamageMod == 0.25f); } static void test_gate_traffic_is_integer() { sots::sim::TuningTable t; t.PERGATETRAFFIC_DRV_TpGate = 5; t.PERGATETRAFFIC_DRV_GatAmp = 12; ApplyContext ctx = ctx_with_tuning(t); PlayerEconomyState s; ApplyTechEffect(s, TechId::DRV_GatAmp, ctx); CHECK_EQ(s.perGateTraffic, 12); ApplyTechEffect(s, TechId::DRV_TpGate, ctx); CHECK_EQ(s.perGateTraffic, 12); // integer max, the smaller key loses PlayerEconomyState r; r.perGateTraffic = 20; ApplyTechEffect(r, TechId::DRV_GatAmp, ctx); CHECK_EQ(r.perGateTraffic, 20); // an already higher value is kept } static void test_node_bore() { ApplyContext ctx; PlayerEconomyState s; CHECK(!s.hasNodeBoreParams); TechApplyOutcome o = ApplyTechEffect(s, TechId::DRV_REND, ctx); CHECK(o.nodeBoreParamsChanged); CHECK(s.hasNodeBoreParams); CHECK_EQ(s.nodeBoreParams[0], 65); CHECK_EQ(s.nodeBoreParams[1], 35); CHECK_EQ(s.nodeBoreParams[2], 4); // Highest wins, whatever the order: a lower drive researched afterwards changes nothing. o = ApplyTechEffect(s, TechId::DRV_RIP, ctx); CHECK(!o.nodeBoreParamsChanged); CHECK_EQ(s.nodeBoreParams[0], 65); o = ApplyTechEffect(s, TechId::DRV_RAD, ctx); CHECK(o.nodeBoreParamsChanged); CHECK_EQ(s.nodeBoreParams[0], 95); CHECK_EQ(s.nodeBoreParams[1], 60); CHECK_EQ(s.nodeBoreParams[2], 5); // The set is re-derived on every completion, not only on a bore tech's own. PlayerEconomyState t; t.researched.set(static_cast(TechIdIndex(TechId::DRV_RIP))); ApplyTechEffect(t, TechId::IND_Waldo, ctx); CHECK(t.hasNodeBoreParams); CHECK_EQ(t.nodeBoreParams[0], 45); } static void test_capture_designs_is_a_tail_check() { ApplyContext ctx; PlayerEconomyState s; ApplyTechEffect(s, TechId::CCC_SpyBm, ctx); CHECK(!s.Flag(PlayerFlag::CaptureDesigns)); ApplyTechEffect(s, TechId::IND_SlvgTech, ctx); CHECK(s.Flag(PlayerFlag::CaptureDesigns)); // Both already researched but the flag not yet set (a loaded save, say): the next // completion of any tech at all sets it, because the test is in the tail. PlayerEconomyState t; t.researched.set(static_cast(TechIdIndex(TechId::CCC_SpyBm))); t.researched.set(static_cast(TechIdIndex(TechId::IND_SlvgTech))); CHECK(!t.Flag(PlayerFlag::CaptureDesigns)); ApplyTechEffect(t, TechId::IND_HvyPlat, ctx); CHECK(t.Flag(PlayerFlag::CaptureDesigns)); } static void test_translation_known_mask() { ApplyContext ctx; PlayerEconomyState s; CHECK_EQ(s.translationKnownMask, 0u); ApplyTechEffect(s, XenoTechId(XenoLevel::Translation1, Species::Liir), ctx); CHECK_EQ(s.translationKnownMask, 1u << static_cast(Species::Liir)); ApplyTechEffect(s, XenoTechId(XenoLevel::Translation2, Species::Hiver), ctx); CHECK_EQ(s.translationKnownMask, 1u << static_cast(Species::Liir)); // level 2 alone: no bit ApplyTechEffect(s, XenoTechId(XenoLevel::Translation1, Species::Hiver), ctx); CHECK_EQ(s.translationKnownMask, (1u << static_cast(Species::Liir)) | (1u << static_cast(Species::Hiver))); // Sticky: clearing the researched bit and re-running the rebuild does not take it back. s.researched.reset(static_cast(TechIdIndex(XenoTechId(XenoLevel::Translation1, Species::Liir)))); RebuildSpeciesTechFlags(s); CHECK(s.translationKnownMask & (1u << static_cast(Species::Liir))); CHECK_EQ(s.speciesFlags[static_cast(Species::Liir)], 0u); } static void test_tail_runs_for_unkeyed_techs() { // A tech outside the 196-name key space has no branch, but the callback still runs its // whole tail: the capture-designs pair test, the flag words and the bore selection. PlayerEconomyState s; s.researched.set(static_cast(TechIdIndex(TechId::CCC_SpyBm))); s.researched.set(static_cast(TechIdIndex(TechId::IND_SlvgTech))); s.researched.set(static_cast(TechIdIndex(TechId::DRV_REND))); s.researched.set(static_cast(TechIdIndex(XenoTechId(XenoLevel::Translation1, Species::Liir)))); const TechApplyOutcome o = RunCompletionTail(s); CHECK(o.applied); CHECK(s.Flag(PlayerFlag::CaptureDesigns)); CHECK(s.hasNodeBoreParams && s.nodeBoreParams[0] == 65); CHECK(o.nodeBoreParamsChanged); CHECK_EQ(s.speciesFlags[static_cast(Species::Liir)], 1u); CHECK_EQ(s.translationKnownMask, 1u << static_cast(Species::Liir)); } static void test_completion_has_no_guard() { ApplyContext ctx; // The callback runs after the node is already marked researched, so it cannot carry an // already-researched guard. ApplyTechEffect (the caller-facing wrapper) still does. PlayerEconomyState s; ApplyTechCompletion(s, TechId::IND_HvyPlat, ctx); const float once = s.outMod; TechApplyOutcome o = ApplyTechCompletion(s, TechId::IND_HvyPlat, ctx); CHECK(o.applied); CHECK(s.outMod != once); // applied a second time PlayerEconomyState g; ApplyTechEffect(g, TechId::IND_HvyPlat, ctx); const float g1 = g.outMod; o = ApplyTechEffect(g, TechId::IND_HvyPlat, ctx); CHECK(!o.applied); CHECK(g.outMod == g1); } static void test_design_option_ids() { // The two tables key on tech ids in the executable; the by-name and by-id builders must // therefore agree bit for bit. for (int i = 0; i < kDesignOptionCountA; ++i) { const char* n = TechIdName(kDesignOptionIdsA[i]); CHECK(n != nullptr && std::string(n) == kDesignOptionNamesA[i]); } for (int i = 0; i < kDesignOptionCountB; ++i) { const char* n = TechIdName(kDesignOptionIdsB[i]); CHECK(n != nullptr && std::string(n) == kDesignOptionNamesB[i]); } // Anchors that were known from unrelated readers before the tables were dumped. CHECK(kDesignOptionIdsA[28] == TechId::CCC_AdvSens); CHECK(kDesignOptionIdsB[12] == TechId::DRV_RIP); CHECK(kDesignOptionIdsB[15] == TechId::BIO_CONNAN); CHECK(kDesignOptionIdsB[21] == TechId::IND_TRKSTL); CHECK(kDesignOptionIdsB[28] == TechId::WEP_HvyPmsl); std::set have = {static_cast(TechId::IND_REFCOAT), static_cast(TechId::SLD_INTANG), static_cast(TechId::DRV_NODE), static_cast(TechId::WEP_HvyPmsl)}; DesignOptionMasks m = ComputeDesignOptionMasks( [&](TechId id) { return have.count(static_cast(id)) > 0; }); CHECK_EQ(m.a, (1u << 0) | (1u << 15)); CHECK_EQ(m.b, (1u << 0) | (1u << 28)); m = ComputeDesignOptionMasks([](TechId) { return true; }); CHECK_EQ(m.a, 0xffffffffu); CHECK_EQ(m.b, 0x1fffffffu); } int main() { test_ids(); test_industrial(); test_biology(); test_gates_and_casting(); test_ai(); test_flags_and_species(); test_plague(); test_xenotech(); test_by_name(); test_table_shape(); test_float32_state(); test_gate_traffic_is_integer(); test_node_bore(); test_capture_designs_is_a_tail_check(); test_translation_known_mask(); test_tail_runs_for_unkeyed_techs(); test_completion_has_no_guard(); test_design_option_ids(); return simtest::finish("test_effects"); }