// B1 adapter tests: ServerPlayer snapshot -> sim::BudgetInputs -> the game's 22-int budget array. // // Hand-written fixtures only. Two of them are the turn-2 state of the reference save's two // landed players, taken from the save dump's own field names, so the mapping is exercised on // numbers the live compare will also see. #include "shim/hooks/budget_inputs.h" #include #include "check.h" using namespace shim::hooks; using sots::sim::Budget; using sots::sim::BudgetInputs; namespace { BudgetSnapshot Blank() { BudgetSnapshot s; s.playerIndex = 0; s.species = 0; s.ownedSystems = 1; return s; } void test_aid_aggregation() { BudgetSnapshot s = Blank(); s.aidCount = 3; s.aid[0] = {30, 1, 500, 1}; // active on both channels s.aid[1] = {40, 0, 700, 0}; // inactive: contributes nothing s.aid[2] = {50, 2, 900, 3}; // active (gate is "> 0", not "== 1") CHECK_EQ(AidResearchPercent(s), 80); CHECK_EQ(AidSavings(s), 1400); s.aid[0].researchPercent = 90; // 90 + 50 = 140 clamps to 100 CHECK_EQ(AidResearchPercent(s), 100); s.aidCount = 0; CHECK_EQ(AidResearchPercent(s), 0); CHECK_EQ(AidSavings(s), 0); } void test_input_mapping() { BudgetSnapshot s = Blank(); s.savings = 289688; s.resRate = 0.25f; s.resMod = 1.0f; s.resScl = 1.0f; s.trm = 0.f; s.shrm = 0.f; s.tra = 3; s.trp = 4; s.setupIncomeMult = 1.5f; s.setupResearchMult = 2.0f; s.isAI = true; s.hasResearchTarget = true; s.aggSystemIncomePositive = 239189; s.aggSystemIncomeNegative = 17; s.aggTradeIncome = 11; s.aggShipCarriedPopIncome = 22; s.aggSecondaryManagerIncome = 33; s.maintenance = 500; s.maintenanceDivisor = 2.0; s.aggConstructionSpend = 4000; s.expenseCount = 2; s.expenses[0] = {7, 100, 900, 0.10f}; s.expenses[1] = {8, 0, 0, 0.25f}; const BudgetInputs in = ToBudgetInputs(s); CHECK_EQ(in.savings, 289688); CHECK(in.ownsSystems); CHECK_EQ(static_cast(in.systemIncome.size()), 2); CHECK_EQ(in.systemIncome[0], 239189); CHECK_EQ(in.systemIncome[1], -17); CHECK_EQ(in.tradeIncome, 11); CHECK_EQ(in.shipCarriedPopIncome, 22); CHECK_EQ(in.secondaryManagerIncome, 33); CHECK_EQ(in.maintenance, 500); CHECK_NEAR(in.maintenanceDivisor, 2.0, 1e-12); CHECK_EQ(in.constructionDemand, 4000); CHECK(in.isAI); CHECK(in.hasResearchTarget); CHECK_NEAR(in.researchRate, 0.25, 1e-9); CHECK_NEAR(in.techIncomeMult, 1.5, 1e-9); CHECK_NEAR(in.techResearchMult, 2.0, 1e-9); CHECK_EQ(in.tra, 3); CHECK_EQ(in.trp, 4); CHECK_EQ(static_cast(in.expenses.size()), 2); CHECK_EQ(in.expenses[0].minimum, 100); CHECK_EQ(in.expenses[0].maximum, 900); CHECK_NEAR(in.expenses[0].fraction, 0.10f, 1e-7); // No systems -> no savings interest. s.ownedSystems = 0; CHECK(!ToBudgetInputs(s).ownsSystems); // A zero aggregate must not become a phantom system in the list. s.ownedSystems = 1; s.aggSystemIncomeNegative = 0; CHECK_EQ(static_cast(ToBudgetInputs(s).systemIncome.size()), 1); } void test_slot_layout() { // The six slots the milestone consumes as inputs. const int inputs[] = {1, 2, 3, 4, 7, 11}; for (int i = 0; i < kBudgetSlots; ++i) { bool expected = false; for (int k : inputs) expected = expected || (k == i); CHECK_EQ(IsInputSlot(i), expected); } } // Player 0 of the reference save at turn 2: human, one system, no research target, no expense // sliders, no aid, treasury 289,688, ResRate 0.25. void test_reference_player0() { BudgetSnapshot s = Blank(); s.savings = 289688; s.resRate = 0.25f; s.resMod = 1.0f; s.resScl = 1.0f; s.isAI = false; s.hasResearchTarget = false; s.aggSystemIncomePositive = 239189; s.aggConstructionSpend = 0; const Budget b = sots::sim::ComputeBudget(ToBudgetInputs(s), s.projected); std::int32_t out[kBudgetSlots]; FillSlots(b, s, out); CHECK_EQ(out[kSlotSavings], 289688); CHECK_EQ(out[kSlotSystemIncomePositive], 239189); CHECK_EQ(out[kSlotSavingsInterest], 2896); // ftol(289688 x 0.01) CHECK_EQ(out[kSlotDebtInterest], 0); CHECK_EQ(out[kSlotExpenses], 0); // available = 239189 + 2896 CHECK_EQ(out[kSlotAvailable], 242085); CHECK_EQ(out[kSlotConstruction], 0); CHECK_EQ(out[kSlotResearchMoney], 60521); // ftol(242085 x 0.25) // 60521/50 x 1.15 x 0.5 x 0.85 = 591.59... -> 591 CHECK_EQ(out[kSlotResearchPoints], 591); CHECK_EQ(out[kSlotResearchMoneyGiven], 0); CHECK_EQ(out[kSlotTotalResearchPoints], 591); CHECK_EQ(out[kSlotBonusIncome], 0); // setup income multiplier is 1 CHECK_EQ(out[kSlotSavingsGiven], 0); // No research target: the research money is reported but never charged, so the whole // income lands in the treasury (the B1 trace's player 0). CHECK_EQ(out[kSlotResearchMoneyKept], 0); CHECK_EQ(b.net, 242085); } // Player 1: the AI, treasury 92,651, ResRate 0.8, ResMod 0.9, Maint 500, a research target. void test_reference_player1() { BudgetSnapshot s = Blank(); s.playerIndex = 1; s.species = 2; s.isAI = true; s.savings = 92651; s.resRate = 0.800000011920929f; s.resMod = 0.8999999761581421f; s.resScl = 1.0f; s.hasResearchTarget = true; s.aggSystemIncomePositive = 100000; s.maintenance = 500; s.maintenanceDivisor = kDifficultyAI.maintenanceDivisor; s.researchDifficultyMult = kDifficultyAI.researchMult; const Budget b = sots::sim::ComputeBudget(ToBudgetInputs(s), s.projected); std::int32_t out[kBudgetSlots]; FillSlots(b, s, out); CHECK_EQ(out[kSlotSavingsInterest], 926); CHECK_EQ(out[kSlotMaintenance], 166); // 500 / ftol(3.0) CHECK_EQ(out[kSlotAvailable], 100760); // 100000 + 926 - 166 CHECK_EQ(out[kSlotConstruction], 0); // AI: the construction slot stays 0 here CHECK(b.hasResearchAllocation); CHECK(out[kSlotResearchMoney] > 0); CHECK_EQ(out[kSlotResearchMoneyKept], out[kSlotResearchMoney]); } // The difficulty row the original picks: the AI row only for a player that is AI and not NPC. void test_difficulty_row() { CHECK_NEAR(DifficultyRowFor(false, false).maintenanceDivisor, 1.0, 1e-12); CHECK_NEAR(DifficultyRowFor(false, false).researchMult, 1.0, 1e-12); CHECK_NEAR(DifficultyRowFor(true, false).maintenanceDivisor, 3.0, 1e-12); CHECK_NEAR(DifficultyRowFor(true, false).researchMult, 1.5, 1e-12); CHECK_NEAR(DifficultyRowFor(true, true).maintenanceDivisor, 1.0, 1e-12); // NPC: human row CHECK_NEAR(DifficultyRowFor(false, true).researchMult, 1.0, 1e-12); } // The research-allocation vector at the tail of the Budget object: one 8-byte element exactly // when the player has a research target. void test_alloc_vector() { BudgetSnapshot s = Blank(); s.aggSystemIncomePositive = 1000; std::uint32_t words[3] = {0xdeadbeef, 0xdeadbeef, 0xdeadbeef}; s.hasResearchTarget = false; FillAllocVector(sots::sim::ComputeBudget(ToBudgetInputs(s), false), words); CHECK_EQ(AllocElementCount(words), 0); s.hasResearchTarget = true; FillAllocVector(sots::sim::ComputeBudget(ToBudgetInputs(s), false), words); CHECK_EQ(AllocElementCount(words), 1); // The original's words are heap pointers; only the span matters. const std::uint32_t live[3] = {888397232u, 888397240u, 888397240u}; CHECK_EQ(AllocElementCount(live), 1); const std::uint32_t empty[3] = {0u, 0u, 0u}; CHECK_EQ(AllocElementCount(empty), 0); const std::uint32_t bad[3] = {8u, 0u, 0u}; CHECK_EQ(AllocElementCount(bad), -1); const shim::trace::Tv d = DescribeAllocVector(live, sizeof live, 256); CHECK_EQ(static_cast(d.items[0].i), 1); } void test_describers() { std::int32_t slots[kBudgetSlots]; for (int i = 0; i < kBudgetSlots; ++i) slots[i] = i * 10; const shim::trace::Tv t = DescribeBudgetSlots(slots, sizeof slots, 256); CHECK_EQ(static_cast(t.keys.size()), static_cast(kBudgetSlots)); CHECK(t.keys[kSlotResearchPoints] == "researchPoints"); CHECK_EQ(t.items[kSlotResearchPoints].i, 170); BudgetSnapshot s = Blank(); s.savings = 42; s.expenseCount = 1; s.expenses[0] = {1, 2, 3, 0.5f}; s.aidCount = 1; s.aid[0] = {25, 1, 60, 1}; const shim::trace::Tv d = DescribeSnapshot(&s, sizeof s, 256); bool sawSavings = false, sawAidPct = false; for (std::size_t i = 0; i < d.keys.size(); ++i) { if (d.keys[i] == "savings") { sawSavings = true; CHECK_EQ(d.items[i].i, 42); } if (d.keys[i] == "aidResearchPercent") { sawAidPct = true; CHECK_EQ(d.items[i].i, 25); } } CHECK(sawSavings); CHECK(sawAidPct); // A short region must not read past the buffer. const shim::trace::Tv bad = DescribeSnapshot(&s, 4, 256); CHECK_EQ(static_cast(bad.keys.size()), 1); } } // namespace int main() { test_aid_aggregation(); test_input_mapping(); test_slot_layout(); test_reference_player0(); test_reference_player1(); test_difficulty_row(); test_alloc_vector(); test_describers(); return simtest::finish("shim_budget"); }