Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/s25main/GlobalGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ void GlobalGameSettings::registerAllAddons()
AddonMoreAnimals,
AddonNoAlliedPush,
AddonNoCoinsDefault,
AddonSingleSoldierCoinTraining,
AddonNumScoutsExploration,
AddonPeacefulMode,
AddonRefundMaterials,
Expand Down
17 changes: 17 additions & 0 deletions libs/s25main/addons/AddonSingleSoldierCoinTraining.h
Original file line number Diff line number Diff line change
@@ -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 soldiers."))
{}
};
1 change: 1 addition & 0 deletions libs/s25main/addons/Addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "addons/AddonDefenderBehavior.h"

#include "addons/AddonNoCoinsDefault.h"
#include "addons/AddonSingleSoldierCoinTraining.h"

#include "addons/AddonAdjustMilitaryStrength.h"

Expand Down
2 changes: 2 additions & 0 deletions libs/s25main/addons/const_addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x

FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000,

SINGLE_SOLDIER_COIN_TRAINING = 0x01100002,

FORESTER_REACH_RADIUS = 0x01200000, WOODCUTTER_REACH_RADIUS = 0x01200001,
STONEMASON_REACH_RADIUS = 0x01200002)
//-V:AddonId:801
Expand Down
51 changes: 32 additions & 19 deletions libs/s25main/buildings/nobMilitary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
#include "gameData/MilitaryConsts.h"
#include "gameData/SettingTypeConv.h"
#include "s25util/Log.h"
#include <algorithm>
#include <limits>
#include <stdexcept>

Expand Down Expand Up @@ -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<std::unique_ptr<nofPassiveSoldier>> 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?
Expand Down
55 changes: 55 additions & 0 deletions tests/s25Main/integration/testAttacking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ auto calcSum(const T& collection)
return std::accumulate(std::begin(collection), std::end(collection), 0u);
}

std::array<unsigned, NUM_SOLDIER_RANKS> CountTroopsByRank(const nobMilitary& bld)
{
std::array<unsigned, NUM_SOLDIER_RANKS> counts{};
for(const auto& soldier : bld.GetTroops())
++counts[soldier.GetRank()];
return counts;
}

void DeliverCoin(nobMilitary& bld, GameWorld& world, const MapPoint hqPos)
{
auto* hq = world.GetSpecObj<nobBaseWarehouse>(hqPos);
BOOST_TEST_REQUIRE(hq);

hq->AddToInventory(GoodCounts::make(GoodType::Coins, 1), true);
bld.SearchCoins();
}

/// Reschedule the walk event of the obj to be executed in numGFs GFs
void rescheduleWalkEvent(TestEventManager& em, noMovable& obj, unsigned numGFs)
{
Expand Down Expand Up @@ -565,6 +582,44 @@ 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);

BuildRoadForBlds(milBld0Pos, hqPos[0]);
DeliverCoin(*milBld0, world, hqPos[0]);

RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[3] == 1u);

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);

BuildRoadForBlds(milBld0Pos, hqPos[0]);
DeliverCoin(*milBld0, world, hqPos[0]);

RTTR_EXEC_TILL(5000, CountTroopsByRank(*milBld0)[1] == 2u);

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(TroopLimitKeepsOrderedRestrictedSoldier, AttackFixture<>)
{
MilitarySettings milSettings = MILITARY_SETTINGS_SCALE;
Expand Down