From b8c1f504aad3de6c819ab56053ab7ef75cc5dcc0 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 17:43:17 +0200 Subject: [PATCH 1/4] Add single-soldier coin training addon --- libs/s25main/GlobalGameSettings.cpp | 1 + .../addons/AddonSingleSoldierCoinTraining.h | 18 ++++++ libs/s25main/addons/Addons.h | 1 + libs/s25main/addons/const_addons.h | 2 +- libs/s25main/buildings/nobMilitary.cpp | 51 ++++++++++------- tests/s25Main/integration/testAttacking.cpp | 55 +++++++++++++++++++ 6 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 libs/s25main/addons/AddonSingleSoldierCoinTraining.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 63f240221e..4cbba30864 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -91,6 +91,7 @@ void GlobalGameSettings::registerAllAddons() AddonMoreAnimals, AddonNoAlliedPush, AddonNoCoinsDefault, + AddonSingleSoldierCoinTraining, AddonNumScoutsExploration, AddonPeacefulMode, AddonRefundMaterials, diff --git a/libs/s25main/addons/AddonSingleSoldierCoinTraining.h b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h new file mode 100644 index 0000000000..27ed5ce53d --- /dev/null +++ b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h @@ -0,0 +1,18 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonBool.h" +#include "mygettext/mygettext.h" + +class AddonSingleSoldierCoinTraining : public AddonBool +{ +public: + AddonSingleSoldierCoinTraining() + : AddonBool(AddonId::SINGLE_SOLDIER_COIN_TRAINING, AddonGroup::Military, + _("Coins train only one soldier"), + _("Gold coins promote only one lowest-rank soldier instead of all eligible lower-rank soldiers.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 4b773587d0..21058c69da 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -28,6 +28,7 @@ #include "addons/AddonDefenderBehavior.h" #include "addons/AddonNoCoinsDefault.h" +#include "addons/AddonSingleSoldierCoinTraining.h" #include "addons/AddonAdjustMilitaryStrength.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 4ae5da8c88..458423b6a0 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -75,7 +75,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x AUTOFLAGS = 0x00F00000, WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002, - ARMOR_CAPTURED_BLD = 0x01000003) + ARMOR_CAPTURED_BLD = 0x01000003, SINGLE_SOLDIER_COIN_TRAINING = 0x01000005) //-V:AddonId:801 enum class AddonGroup : unsigned diff --git a/libs/s25main/buildings/nobMilitary.cpp b/libs/s25main/buildings/nobMilitary.cpp index 6ca29ccaac..858c9b6d15 100644 --- a/libs/s25main/buildings/nobMilitary.cpp +++ b/libs/s25main/buildings/nobMilitary.cpp @@ -36,6 +36,7 @@ #include "gameData/MilitaryConsts.h" #include "gameData/SettingTypeConv.h" #include "s25util/Log.h" +#include #include #include @@ -315,28 +316,40 @@ void nobMilitary::HandleEvent(const unsigned id) { upgrade_event = nullptr; - // Soldaten befördern - // Von hinten durchgehen - // Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat, - // wird dieser ebenfalls befördert usw.! std::vector> soldiersToUpgrade; - // Rang des letzten beförderten Soldaten, MaxRank am Anfang setzen, damit keiner über den maximalen Rang - // befördert wird - uint8_t last_rank = world->GetGGS().GetMaxMilitaryRank(); - for(auto it = troops.rbegin(); it != troops.rend();) + const uint8_t maxRank = world->GetGGS().GetMaxMilitaryRank(); + + if(world->GetGGS().isEnabled(AddonId::SINGLE_SOLDIER_COIN_TRAINING)) { - auto& soldier = *it; - // Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang - // als der letzte haben, damit er auch noch befördert werden kann - if(soldier->GetRank() < last_rank) + auto it = std::find_if(troops.begin(), troops.end(), + [maxRank](const auto& soldier) { return soldier->GetRank() < maxRank; }); + if(it != troops.end()) { - // Rang merken - last_rank = soldier->GetRank(); - // Remove from sorted container as changing it breaks sorting - soldiersToUpgrade.push_back(std::move(soldier)); - it = helpers::erase_reverse(troops, it); - } else - ++it; + soldiersToUpgrade.push_back(std::move(*it)); + troops.erase(it); + } + } else + { + // Soldaten befördern + // Von hinten durchgehen + // Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat, + // wird dieser ebenfalls befördert usw.! + uint8_t last_rank = maxRank; + for(auto it = troops.rbegin(); it != troops.rend();) + { + auto& soldier = *it; + // Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang + // als der letzte haben, damit er auch noch befördert werden kann + if(soldier->GetRank() < last_rank) + { + // Rang merken + last_rank = soldier->GetRank(); + // Remove from sorted container as changing it breaks sorting + soldiersToUpgrade.push_back(std::move(soldier)); + it = helpers::erase_reverse(troops, it); + } else + ++it; + } } // Wurde jemand befördert? diff --git a/tests/s25Main/integration/testAttacking.cpp b/tests/s25Main/integration/testAttacking.cpp index 975d9c4cc1..95fbd8dc72 100644 --- a/tests/s25Main/integration/testAttacking.cpp +++ b/tests/s25Main/integration/testAttacking.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include using SoldierState = nofActiveSoldier::SoldierState; @@ -61,6 +62,24 @@ auto calcSum(const T& collection) return std::accumulate(std::begin(collection), std::end(collection), 0u); } +std::array CountTroopsByRank(const nobMilitary& bld) +{ + std::array counts{}; + for(const auto& soldier : bld.GetTroops()) + ++counts[soldier.GetRank()]; + return counts; +} + +void DeliverCoin(nobMilitary& bld, GameWorld& world) +{ + auto coin = std::make_unique(GoodType::Coins, &bld, &bld); + world.GetPlayer(bld.GetPlayer()).RegisterWare(*coin); + bld.TakeWare(coin.get()); + bld.AddWare(std::move(coin)); + + BOOST_TEST_REQUIRE(bld.GetNumCoins() == 1u); +} + /// Reschedule the walk event of the obj to be executed in numGFs GFs void rescheduleWalkEvent(TestEventManager& em, noMovable& obj, unsigned numGFs) { @@ -543,6 +562,42 @@ BOOST_FIXTURE_TEST_CASE(ArmoredSoldierLosesArmorInFight, AttackFixture<>) BOOST_TEST(milBld1->GetDefender()->GetHitpoints() == HITPOINTS[milBld1->GetDefender()->GetRank()]); } +BOOST_FIXTURE_TEST_CASE(CoinTrainingUpgradesRankChainByDefault, AttackFixture<>) +{ + AddSoldiers(milBld0Pos, 1, Job::Private); + AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); + AddSoldiers(milBld0Pos, 1, Job::Sergeant); + + DeliverCoin(*milBld0, world); + + RTTR_EXEC_TILL(5000, milBld0->GetNumCoins() == 0u); + + const auto counts = CountTroopsByRank(*milBld0); + BOOST_TEST_REQUIRE(counts[0] == 0u); + BOOST_TEST_REQUIRE(counts[1] == 1u); + BOOST_TEST_REQUIRE(counts[2] == 1u); + BOOST_TEST_REQUIRE(counts[3] == 1u); +} + +BOOST_FIXTURE_TEST_CASE(SingleSoldierCoinTrainingUpgradesOnlyLowestRankSoldier, AttackFixture<>) +{ + this->ggs.setSelection(AddonId::SINGLE_SOLDIER_COIN_TRAINING, 1); + + AddSoldiers(milBld0Pos, 1, Job::Private); + AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); + AddSoldiers(milBld0Pos, 1, Job::Sergeant); + + DeliverCoin(*milBld0, world); + + RTTR_EXEC_TILL(5000, milBld0->GetNumCoins() == 0u); + + const auto counts = CountTroopsByRank(*milBld0); + BOOST_TEST_REQUIRE(counts[0] == 0u); + BOOST_TEST_REQUIRE(counts[1] == 2u); + BOOST_TEST_REQUIRE(counts[2] == 1u); + BOOST_TEST_REQUIRE(counts[3] == 0u); +} + BOOST_FIXTURE_TEST_CASE(ConquerBldCoinAddonEnable, AttackFixture<>) { this->ggs.setSelection(AddonId::COINS_CAPTURED_BLD, 1); // addon is active on second run From 7486af7dca226aa75f82d6d9081097e2a894d4d9 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 2 May 2026 11:23:47 +0200 Subject: [PATCH 2/4] Use delivered coin in coin training tests --- tests/s25Main/integration/testAttacking.cpp | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/s25Main/integration/testAttacking.cpp b/tests/s25Main/integration/testAttacking.cpp index 95fbd8dc72..ebb3d67397 100644 --- a/tests/s25Main/integration/testAttacking.cpp +++ b/tests/s25Main/integration/testAttacking.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include using SoldierState = nofActiveSoldier::SoldierState; @@ -70,14 +69,13 @@ std::array CountTroopsByRank(const nobMilitary& bld return counts; } -void DeliverCoin(nobMilitary& bld, GameWorld& world) +void DeliverCoin(nobMilitary& bld, GameWorld& world, const MapPoint hqPos) { - auto coin = std::make_unique(GoodType::Coins, &bld, &bld); - world.GetPlayer(bld.GetPlayer()).RegisterWare(*coin); - bld.TakeWare(coin.get()); - bld.AddWare(std::move(coin)); + auto* hq = world.GetSpecObj(hqPos); + BOOST_TEST_REQUIRE(hq); - BOOST_TEST_REQUIRE(bld.GetNumCoins() == 1u); + hq->AddToInventory(GoodCounts::make(GoodType::Coins, 1), true); + bld.SearchCoins(); } /// Reschedule the walk event of the obj to be executed in numGFs GFs @@ -568,9 +566,10 @@ BOOST_FIXTURE_TEST_CASE(CoinTrainingUpgradesRankChainByDefault, AttackFixture<>) AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); AddSoldiers(milBld0Pos, 1, Job::Sergeant); - DeliverCoin(*milBld0, world); + BuildRoadForBlds(milBld0Pos, hqPos[0]); + DeliverCoin(*milBld0, world, hqPos[0]); - RTTR_EXEC_TILL(5000, milBld0->GetNumCoins() == 0u); + RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[3] == 1u); const auto counts = CountTroopsByRank(*milBld0); BOOST_TEST_REQUIRE(counts[0] == 0u); @@ -587,9 +586,10 @@ BOOST_FIXTURE_TEST_CASE(SingleSoldierCoinTrainingUpgradesOnlyLowestRankSoldier, AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass); AddSoldiers(milBld0Pos, 1, Job::Sergeant); - DeliverCoin(*milBld0, world); + BuildRoadForBlds(milBld0Pos, hqPos[0]); + DeliverCoin(*milBld0, world, hqPos[0]); - RTTR_EXEC_TILL(5000, milBld0->GetNumCoins() == 0u); + RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[1] == 2u); const auto counts = CountTroopsByRank(*milBld0); BOOST_TEST_REQUIRE(counts[0] == 0u); From 707fef96a1631756d1e6df6f488d44bf04f7137a Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 2 May 2026 22:26:33 +0200 Subject: [PATCH 3/4] Update libs/s25main/addons/AddonSingleSoldierCoinTraining.h Co-authored-by: Alexander Grund --- libs/s25main/addons/AddonSingleSoldierCoinTraining.h | 5 ++--- libs/s25main/addons/const_addons.h | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libs/s25main/addons/AddonSingleSoldierCoinTraining.h b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h index 27ed5ce53d..e8f4080297 100644 --- a/libs/s25main/addons/AddonSingleSoldierCoinTraining.h +++ b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h @@ -11,8 +11,7 @@ class AddonSingleSoldierCoinTraining : public AddonBool { public: AddonSingleSoldierCoinTraining() - : AddonBool(AddonId::SINGLE_SOLDIER_COIN_TRAINING, AddonGroup::Military, - _("Coins train only one soldier"), - _("Gold coins promote only one lowest-rank soldier instead of all eligible lower-rank soldiers.")) + : AddonBool(AddonId::SINGLE_SOLDIER_COIN_TRAINING, AddonGroup::Military, _("Coins train only one soldier"), + _("Gold coins promote only one lowest-rank soldier instead of all eligible soldiers.")) {} }; diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 458423b6a0..456e195ac4 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -26,6 +26,7 @@ // 00E Jonathan // 00F Jarno // 010 aztimh +// 011 DevOpsOfChaos // Do not forget to add your Addon to GlobalGameSettings::registerAllAddons @ GlobalGameSettings.cpp! // Never use a number twice! @@ -75,7 +76,9 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x AUTOFLAGS = 0x00F00000, WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002, - ARMOR_CAPTURED_BLD = 0x01000003, SINGLE_SOLDIER_COIN_TRAINING = 0x01000005) + ARMOR_CAPTURED_BLD = 0x01000003, + + SINGLE_SOLDIER_COIN_TRAINING = 0x01100002) //-V:AddonId:801 enum class AddonGroup : unsigned From 942d7b3a35eba2c0b842a094578c390317763eeb Mon Sep 17 00:00:00 2001 From: Manu Date: Mon, 22 Jun 2026 14:40:07 +0200 Subject: [PATCH 4/4] Assign unique ID to single-soldier coin training addon --- libs/s25main/addons/const_addons.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index fe170b4476..da598bf301 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -81,7 +81,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, - SINGLE_SOLDIER_COIN_TRAINING = 0x01100001, + SINGLE_SOLDIER_COIN_TRAINING = 0x01100002, FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001, STONEMASON_REACH_RADIUS = 0x01200002)