From ac202a7c26266ced8d08880f70462c331453311b Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 17:43:17 +0200 Subject: [PATCH 1/3] Add single-soldier coin training addon --- libs/s25main/GlobalGameSettings.cpp | 1 + .../addons/AddonSingleSoldierCoinTraining.h | 17 ++++++ libs/s25main/addons/Addons.h | 1 + libs/s25main/addons/const_addons.h | 5 +- libs/s25main/buildings/nobMilitary.cpp | 51 ++++++++++------- tests/s25Main/integration/testAttacking.cpp | 55 +++++++++++++++++++ 6 files changed, 108 insertions(+), 22 deletions(-) create mode 100644 libs/s25main/addons/AddonSingleSoldierCoinTraining.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index d0fa0d3e0c..dd6801c566 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -102,6 +102,7 @@ void GlobalGameSettings::registerAllAddons() AddonMoreAnimals, AddonNoAlliedPush, AddonNoCoinsDefault, + AddonSingleSoldierCoinTraining, AddonStrandedSoldierReturnSearch, AddonNumScoutsExploration, AddonPeacefulMode, diff --git a/libs/s25main/addons/AddonSingleSoldierCoinTraining.h b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h new file mode 100644 index 0000000000..175e00c0a8 --- /dev/null +++ b/libs/s25main/addons/AddonSingleSoldierCoinTraining.h @@ -0,0 +1,17 @@ +// 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 b5f912e1de..6684612600 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/AddonStrandedSoldierReturnSearch.h" #include "addons/AddonAdjustMilitaryStrength.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 31165b45e5..4c907c3dbe 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -83,9 +83,8 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002, ARMOR_CAPTURED_BLD = 0x01000003, - FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, - - STRANDED_SOLDIER_RETURN_SEARCH = 0x01100001, + FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, STRANDED_SOLDIER_RETURN_SEARCH = 0x01100001, + SINGLE_SOLDIER_COIN_TRAINING = 0x01100002, FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001, STONEMASON_REACH_RADIUS = 0x01200002) diff --git a/libs/s25main/buildings/nobMilitary.cpp b/libs/s25main/buildings/nobMilitary.cpp index 47c8a7b05e..d3a44f1cf4 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 f3eede60c5..797b928aac 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) { @@ -601,6 +620,42 @@ BOOST_FIXTURE_TEST_CASE(TroopLimitKeepsOrderedRestrictedSoldier, AttackFixture<> BOOST_TEST_REQUIRE(milBld0->GetLeavingFigures().empty()); } +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 5b25ed68b94a1fc2552bc3858d6568de9fe415d2 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 2 May 2026 11:23:47 +0200 Subject: [PATCH 2/3] 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 797b928aac..9ae043271b 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 @@ -626,9 +624,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); @@ -645,9 +644,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 508762a801aec8ed9bad57637a468038f8841311 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 23:36:14 +0200 Subject: [PATCH 3/3] Update languages submodule --- external/languages | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/languages b/external/languages index b5e089c144..10a4059360 160000 --- a/external/languages +++ b/external/languages @@ -1 +1 @@ -Subproject commit b5e089c14457ff730976bd01257bf3b8eca364d6 +Subproject commit 10a4059360a3900f6430fecaa7a468fa5822234b