Skip to content

Commit e4ec788

Browse files
authored
Merge pull request #1928 from DevOpsOfChaos/sidequest/single-soldier-coin-training
Add single-soldier coin training addon
2 parents 59327b2 + 508762a commit e4ec788

7 files changed

Lines changed: 109 additions & 23 deletions

File tree

‎libs/s25main/GlobalGameSettings.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ void GlobalGameSettings::registerAllAddons()
102102
AddonMoreAnimals,
103103
AddonNoAlliedPush,
104104
AddonNoCoinsDefault,
105+
AddonSingleSoldierCoinTraining,
105106
AddonStrandedSoldierReturnSearch,
106107
AddonNumScoutsExploration,
107108
AddonPeacefulMode,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2+
//
3+
// SPDX-License-Identifier: GPL-2.0-or-later
4+
5+
#pragma once
6+
7+
#include "AddonBool.h"
8+
#include "mygettext/mygettext.h"
9+
10+
class AddonSingleSoldierCoinTraining : public AddonBool
11+
{
12+
public:
13+
AddonSingleSoldierCoinTraining()
14+
: AddonBool(AddonId::SINGLE_SOLDIER_COIN_TRAINING, AddonGroup::Military, _("Coins train only one soldier"),
15+
_("Gold coins promote only one lowest-rank soldier instead of all eligible lower-rank soldiers."))
16+
{}
17+
};

‎libs/s25main/addons/Addons.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#include "addons/AddonDefenderBehavior.h"
2929

3030
#include "addons/AddonNoCoinsDefault.h"
31+
#include "addons/AddonSingleSoldierCoinTraining.h"
3132
#include "addons/AddonStrandedSoldierReturnSearch.h"
3233

3334
#include "addons/AddonAdjustMilitaryStrength.h"

‎libs/s25main/addons/const_addons.h‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,8 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x
8383
WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002,
8484
ARMOR_CAPTURED_BLD = 0x01000003,
8585

86-
FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000,
87-
88-
STRANDED_SOLDIER_RETURN_SEARCH = 0x01100001,
86+
FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000, STRANDED_SOLDIER_RETURN_SEARCH = 0x01100001,
87+
SINGLE_SOLDIER_COIN_TRAINING = 0x01100002,
8988

9089
FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001,
9190
STONEMASON_REACH_RADIUS = 0x01200002)

‎libs/s25main/buildings/nobMilitary.cpp‎

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include "gameData/MilitaryConsts.h"
3737
#include "gameData/SettingTypeConv.h"
3838
#include "s25util/Log.h"
39+
#include <algorithm>
3940
#include <limits>
4041
#include <stdexcept>
4142

@@ -315,28 +316,40 @@ void nobMilitary::HandleEvent(const unsigned id)
315316
{
316317
upgrade_event = nullptr;
317318

318-
// Soldaten befördern
319-
// Von hinten durchgehen
320-
// Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat,
321-
// wird dieser ebenfalls befördert usw.!
322319
std::vector<std::unique_ptr<nofPassiveSoldier>> soldiersToUpgrade;
323-
// Rang des letzten beförderten Soldaten, MaxRank am Anfang setzen, damit keiner über den maximalen Rang
324-
// befördert wird
325-
uint8_t last_rank = world->GetGGS().GetMaxMilitaryRank();
326-
for(auto it = troops.rbegin(); it != troops.rend();)
320+
const uint8_t maxRank = world->GetGGS().GetMaxMilitaryRank();
321+
322+
if(world->GetGGS().isEnabled(AddonId::SINGLE_SOLDIER_COIN_TRAINING))
327323
{
328-
auto& soldier = *it;
329-
// Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang
330-
// als der letzte haben, damit er auch noch befördert werden kann
331-
if(soldier->GetRank() < last_rank)
324+
auto it = std::find_if(troops.begin(), troops.end(),
325+
[maxRank](const auto& soldier) { return soldier->GetRank() < maxRank; });
326+
if(it != troops.end())
332327
{
333-
// Rang merken
334-
last_rank = soldier->GetRank();
335-
// Remove from sorted container as changing it breaks sorting
336-
soldiersToUpgrade.push_back(std::move(soldier));
337-
it = helpers::erase_reverse(troops, it);
338-
} else
339-
++it;
328+
soldiersToUpgrade.push_back(std::move(*it));
329+
troops.erase(it);
330+
}
331+
} else
332+
{
333+
// Soldaten befördern
334+
// Von hinten durchgehen
335+
// Wenn der nachfolgende (schwächere) Soldat einen niedrigeren Rang hat,
336+
// wird dieser ebenfalls befördert usw.!
337+
uint8_t last_rank = maxRank;
338+
for(auto it = troops.rbegin(); it != troops.rend();)
339+
{
340+
auto& soldier = *it;
341+
// Es wurde schon einer befördert, dieser Soldat muss nun einen niedrigeren Rang
342+
// als der letzte haben, damit er auch noch befördert werden kann
343+
if(soldier->GetRank() < last_rank)
344+
{
345+
// Rang merken
346+
last_rank = soldier->GetRank();
347+
// Remove from sorted container as changing it breaks sorting
348+
soldiersToUpgrade.push_back(std::move(soldier));
349+
it = helpers::erase_reverse(troops, it);
350+
} else
351+
++it;
352+
}
340353
}
341354

342355
// Wurde jemand befördert?

‎tests/s25Main/integration/testAttacking.cpp‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ auto calcSum(const T& collection)
6161
return std::accumulate(std::begin(collection), std::end(collection), 0u);
6262
}
6363

64+
std::array<unsigned, NUM_SOLDIER_RANKS> CountTroopsByRank(const nobMilitary& bld)
65+
{
66+
std::array<unsigned, NUM_SOLDIER_RANKS> counts{};
67+
for(const auto& soldier : bld.GetTroops())
68+
++counts[soldier.GetRank()];
69+
return counts;
70+
}
71+
72+
void DeliverCoin(nobMilitary& bld, GameWorld& world, const MapPoint hqPos)
73+
{
74+
auto* hq = world.GetSpecObj<nobBaseWarehouse>(hqPos);
75+
BOOST_TEST_REQUIRE(hq);
76+
77+
hq->AddToInventory(GoodCounts::make(GoodType::Coins, 1), true);
78+
bld.SearchCoins();
79+
}
80+
6481
/// Reschedule the walk event of the obj to be executed in numGFs GFs
6582
void rescheduleWalkEvent(TestEventManager& em, noMovable& obj, unsigned numGFs)
6683
{
@@ -601,6 +618,44 @@ BOOST_FIXTURE_TEST_CASE(TroopLimitKeepsOrderedRestrictedSoldier, AttackFixture<>
601618
BOOST_TEST_REQUIRE(milBld0->GetLeavingFigures().empty());
602619
}
603620

621+
BOOST_FIXTURE_TEST_CASE(CoinTrainingUpgradesRankChainByDefault, AttackFixture<>)
622+
{
623+
AddSoldiers(milBld0Pos, 1, Job::Private);
624+
AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass);
625+
AddSoldiers(milBld0Pos, 1, Job::Sergeant);
626+
627+
BuildRoadForBlds(milBld0Pos, hqPos[0]);
628+
DeliverCoin(*milBld0, world, hqPos[0]);
629+
630+
RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[3] == 1u);
631+
632+
const auto counts = CountTroopsByRank(*milBld0);
633+
BOOST_TEST_REQUIRE(counts[0] == 0u);
634+
BOOST_TEST_REQUIRE(counts[1] == 1u);
635+
BOOST_TEST_REQUIRE(counts[2] == 1u);
636+
BOOST_TEST_REQUIRE(counts[3] == 1u);
637+
}
638+
639+
BOOST_FIXTURE_TEST_CASE(SingleSoldierCoinTrainingUpgradesOnlyLowestRankSoldier, AttackFixture<>)
640+
{
641+
this->ggs.setSelection(AddonId::SINGLE_SOLDIER_COIN_TRAINING, 1);
642+
643+
AddSoldiers(milBld0Pos, 1, Job::Private);
644+
AddSoldiers(milBld0Pos, 1, Job::PrivateFirstClass);
645+
AddSoldiers(milBld0Pos, 1, Job::Sergeant);
646+
647+
BuildRoadForBlds(milBld0Pos, hqPos[0]);
648+
DeliverCoin(*milBld0, world, hqPos[0]);
649+
650+
RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[1] == 2u);
651+
652+
const auto counts = CountTroopsByRank(*milBld0);
653+
BOOST_TEST_REQUIRE(counts[0] == 0u);
654+
BOOST_TEST_REQUIRE(counts[1] == 2u);
655+
BOOST_TEST_REQUIRE(counts[2] == 1u);
656+
BOOST_TEST_REQUIRE(counts[3] == 0u);
657+
}
658+
604659
BOOST_FIXTURE_TEST_CASE(ConquerBldCoinAddonEnable, AttackFixture<>)
605660
{
606661
this->ggs.setSelection(AddonId::COINS_CAPTURED_BLD, 1); // addon is active on second run

0 commit comments

Comments
 (0)