From ea2ee5f504145668b525c25aadacce132cab15f7 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 09:47:51 +0200 Subject: [PATCH 01/23] Fix inexhaustible granite mine production --- libs/s25main/figures/nofMiner.cpp | 27 ++++++++----- libs/s25main/figures/nofMiner.h | 1 + tests/s25Main/integration/testProduction.cpp | 42 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 912619b58a..65836a8f4e 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -71,23 +71,32 @@ helpers::OptionalEnum nofMiner::ProduceWare() bool nofMiner::AreWaresAvailable() const { - return nofWorkman::AreWaresAvailable() && FindPointWithResource(GetRequiredResType()).isValid(); + return nofWorkman::AreWaresAvailable() + && (CanMineWithoutResource() || FindPointWithResource(GetRequiredResType()).isValid()); } bool nofMiner::StartWorking() { - MapPoint resPt = FindPointWithResource(GetRequiredResType()); - if(!resPt.isValid()) - return false; const GlobalGameSettings& settings = world->GetGGS(); - bool inexhaustibleRes = settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES) - || (workplace->GetBuildingType() == BuildingType::GraniteMine - && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)); - if(!inexhaustibleRes) - world->ReduceResource(resPt); + const bool canMineWithoutResource = CanMineWithoutResource(); + const bool inexhaustibleRes = settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES) || canMineWithoutResource; + if(!canMineWithoutResource) + { + MapPoint resPt = FindPointWithResource(GetRequiredResType()); + if(!resPt.isValid()) + return false; + if(!inexhaustibleRes) + world->ReduceResource(resPt); + } return nofWorkman::StartWorking(); } +bool nofMiner::CanMineWithoutResource() const +{ + return workplace->GetBuildingType() == BuildingType::GraniteMine + && world->GetGGS().isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES); +} + ResourceType nofMiner::GetRequiredResType() const { switch(workplace->GetBuildingType()) diff --git a/libs/s25main/figures/nofMiner.h b/libs/s25main/figures/nofMiner.h index c3fdc810ad..d965a0fdcd 100644 --- a/libs/s25main/figures/nofMiner.h +++ b/libs/s25main/figures/nofMiner.h @@ -23,6 +23,7 @@ class nofMiner : public nofWorkman bool AreWaresAvailable() const override; bool StartWorking() override; ResourceType GetRequiredResType() const; + bool CanMineWithoutResource() const; public: nofMiner(MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 5ce05b76b4..75ad8525a3 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -102,4 +102,46 @@ BOOST_FIXTURE_TEST_CASE(MetalWorkerOrders, WorldWithGCExecution1P) RTTR_EXEC_TILL(1300, mw->is_working); } +BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, WorldWithGCExecution1P) +{ + GoodsAndPeopleCounts inv; + inv[GoodType::Fish] = 10; + inv[GoodType::PickAxe] = 1; + inv[Job::Miner] = 1; + world.GetSpecObj(hqPos)->AddToInventory(inv, true); + + MapPoint minePos = hqPos + MapPoint(2, 0); + const auto* mine = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans)); + this->BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, + std::vector(2, Direction::West)); + + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialStones = curInventory[GoodType::Stones]; + RTTR_EXEC_TILL(500, mine->HasWorker()); + RTTR_SKIP_GFS(2000); + + BOOST_TEST(curInventory[GoodType::Stones] == initialStones); +} + +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineWorksWithoutResources, WorldWithGCExecution1P) +{ + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); + + GoodsAndPeopleCounts inv; + inv[GoodType::Fish] = 10; + inv[GoodType::PickAxe] = 1; + inv[Job::Miner] = 1; + world.GetSpecObj(hqPos)->AddToInventory(inv, true); + + MapPoint minePos = hqPos + MapPoint(2, 0); + BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans); + this->BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, + std::vector(2, Direction::West)); + + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialStones = curInventory[GoodType::Stones]; + RTTR_EXEC_TILL(3000, curInventory[GoodType::Stones] > initialStones); +} + BOOST_AUTO_TEST_SUITE_END() From b887aaf018ef588af1033d8229a1bb79a5b580e6 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 13:50:26 +0200 Subject: [PATCH 02/23] Simplify granite mine resource handling --- libs/s25main/figures/nofMiner.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 65836a8f4e..dfbb2ca1c4 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -78,14 +78,12 @@ bool nofMiner::AreWaresAvailable() const bool nofMiner::StartWorking() { const GlobalGameSettings& settings = world->GetGGS(); - const bool canMineWithoutResource = CanMineWithoutResource(); - const bool inexhaustibleRes = settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES) || canMineWithoutResource; - if(!canMineWithoutResource) + if(!CanMineWithoutResource()) { MapPoint resPt = FindPointWithResource(GetRequiredResType()); if(!resPt.isValid()) return false; - if(!inexhaustibleRes) + if(!settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) world->ReduceResource(resPt); } return nofWorkman::StartWorking(); From 4e4b9d1f0a7a07fb37e6b67311bb18ee18d9f2ef Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 14:17:03 +0200 Subject: [PATCH 03/23] Tighten granite mine production tests --- tests/s25Main/integration/testProduction.cpp | 59 +++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 75ad8525a3..b24e4e54ae 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -23,6 +23,27 @@ static std::ostream& operator<<(std::ostream& os, const PostCategory& cat) BOOST_AUTO_TEST_SUITE(Production) +namespace { +struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P +{ + const nobUsual* CreateGraniteMineWithoutResources() + { + GoodsAndPeopleCounts inv; + inv[GoodType::Fish] = 10; + inv[GoodType::PickAxe] = 1; + inv[Job::Miner] = 1; + world.GetSpecObj(hqPos)->AddToInventory(inv, true); + + MapPoint minePos = hqPos + MapPoint(2, 0); + const auto* mine = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans)); + BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, std::vector(2, Direction::West)); + RTTR_EXEC_TILL(500, mine->HasWorker()); + return mine; + } +}; +} // namespace + BOOST_FIXTURE_TEST_CASE(MetalWorkerStopped, WorldWithGCExecution1P) { addStartResources(); @@ -102,46 +123,18 @@ BOOST_FIXTURE_TEST_CASE(MetalWorkerOrders, WorldWithGCExecution1P) RTTR_EXEC_TILL(1300, mw->is_working); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, WorldWithGCExecution1P) +BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithoutResourcesFixture) { - GoodsAndPeopleCounts inv; - inv[GoodType::Fish] = 10; - inv[GoodType::PickAxe] = 1; - inv[Job::Miner] = 1; - world.GetSpecObj(hqPos)->AddToInventory(inv, true); - - MapPoint minePos = hqPos + MapPoint(2, 0); - const auto* mine = static_cast( - BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans)); - this->BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, - std::vector(2, Direction::West)); - + const nobUsual* mine = CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_EXEC_TILL(500, mine->HasWorker()); - RTTR_SKIP_GFS(2000); + RTTR_SKIP_GFS(2000); BOOST_TEST(curInventory[GoodType::Stones] == initialStones); -} -BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineWorksWithoutResources, WorldWithGCExecution1P) -{ ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); - - GoodsAndPeopleCounts inv; - inv[GoodType::Fish] = 10; - inv[GoodType::PickAxe] = 1; - inv[Job::Miner] = 1; - world.GetSpecObj(hqPos)->AddToInventory(inv, true); - - MapPoint minePos = hqPos + MapPoint(2, 0); - BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans); - this->BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, - std::vector(2, Direction::West)); - - const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_EXEC_TILL(3000, curInventory[GoodType::Stones] > initialStones); + RTTR_EXEC_TILL(2000, mine->is_working || curInventory[GoodType::Stones] > initialStones); + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); } BOOST_AUTO_TEST_SUITE_END() From b636b5dc6a45510645c0ae9723d8ed1f78064e3a Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 14:17:18 +0200 Subject: [PATCH 04/23] Clarify inexhaustible granite mines addon description --- libs/s25main/addons/AddonInexhaustibleGraniteMines.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h index 418d30e791..f12b35a909 100644 --- a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h +++ b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h @@ -8,13 +8,13 @@ #include "mygettext/mygettext.h" /** - * Addon for allowing to have unlimited resources. + * Addon for allowing granite mines to produce independently of stone resources. */ class AddonInexhaustibleGraniteMines : public AddonBool { public: AddonInexhaustibleGraniteMines() : AddonBool(AddonId::INEXHAUSTIBLE_GRANITEMINES, AddonGroup::Economy, _("Inexhaustible Granite Mines"), - _("Granite mines will never be depleted.")) + _("Granite mines can produce stone on any mountain spot and never deplete resources.")) {} }; From 2a4936e3bfc18d5f756c13c7b6a56cad3ceb34f9 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 14:38:45 +0200 Subject: [PATCH 05/23] Avoid runtime addon toggle in granite mine test --- tests/s25Main/integration/testProduction.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index b24e4e54ae..9783f82a53 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -125,15 +125,22 @@ BOOST_FIXTURE_TEST_CASE(MetalWorkerOrders, WorldWithGCExecution1P) BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithoutResourcesFixture) { - const nobUsual* mine = CreateGraniteMineWithoutResources(); + CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; RTTR_SKIP_GFS(2000); + BOOST_TEST(curInventory[GoodType::Stones] == initialStones); +} +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineWorksWithoutResources, GraniteMineWithoutResourcesFixture) +{ ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); - RTTR_EXEC_TILL(2000, mine->is_working || curInventory[GoodType::Stones] > initialStones); + CreateGraniteMineWithoutResources(); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialStones = curInventory[GoodType::Stones]; + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); } From fb40fc9d29b516e7224bb3adce85bc5d64109a32 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 21:11:51 +0200 Subject: [PATCH 06/23] Split granite mine work-everywhere addon --- libs/s25main/GlobalGameSettings.cpp | 1 + .../addons/AddonGraniteMinesWorkEverywhere.h | 21 +++++++ .../addons/AddonInexhaustibleGraniteMines.h | 4 +- libs/s25main/addons/Addons.h | 1 + libs/s25main/addons/const_addons.h | 2 +- libs/s25main/figures/nofMiner.cpp | 60 +++++++++++++++---- libs/s25main/figures/nofMiner.h | 4 +- tests/s25Main/integration/testProduction.cpp | 44 ++++++++++++-- 8 files changed, 116 insertions(+), 21 deletions(-) create mode 100644 libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 63f240221e..c855b37418 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -79,6 +79,7 @@ void GlobalGameSettings::registerAllAddons() AddonHalfCostMilEquip, AddonInexhaustibleFish, AddonInexhaustibleGraniteMines, + AddonGraniteMinesWorkEverywhere, AddonInexhaustibleMines, AddonLimitCatapults, AddonManualRoadEnlargement, diff --git a/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h b/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h new file mode 100644 index 0000000000..ab5bd5c3b8 --- /dev/null +++ b/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h @@ -0,0 +1,21 @@ +// 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 "const_addons.h" +#include "mygettext/mygettext.h" + +/** + * Addon for creating finite granite resources below granite mines without explicit stone resources. + */ +class AddonGraniteMinesWorkEverywhere : public AddonBool +{ +public: + AddonGraniteMinesWorkEverywhere() + : AddonBool(AddonId::GRANITEMINES_WORK_EVERYWHERE, AddonGroup::Economy, _("Granite Mines Work Everywhere"), + _("Granite mines can create a finite stone resource on otherwise empty mountain spots.")) + {} +}; diff --git a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h index f12b35a909..592895651e 100644 --- a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h +++ b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h @@ -8,13 +8,13 @@ #include "mygettext/mygettext.h" /** - * Addon for allowing granite mines to produce independently of stone resources. + * Addon for allowing granite mines to have unlimited resources. */ class AddonInexhaustibleGraniteMines : public AddonBool { public: AddonInexhaustibleGraniteMines() : AddonBool(AddonId::INEXHAUSTIBLE_GRANITEMINES, AddonGroup::Economy, _("Inexhaustible Granite Mines"), - _("Granite mines can produce stone on any mountain spot and never deplete resources.")) + _("Granite mines will never deplete stone resources.")) {} }; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 4b773587d0..ac7f84bf61 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -35,6 +35,7 @@ #include "addons/AddonInexhaustibleFish.h" #include "addons/AddonInexhaustibleGraniteMines.h" +#include "addons/AddonGraniteMinesWorkEverywhere.h" #include "addons/AddonMaxRank.h" #include "addons/AddonMilitaryAid.h" #include "addons/AddonSeaAttack.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 4ae5da8c88..c031b36218 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -55,7 +55,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, - INEXHAUSTIBLE_GRANITEMINES = 0x00800000, + INEXHAUSTIBLE_GRANITEMINES = 0x00800000, GRANITEMINES_WORK_EVERYWHERE = 0x00800001, MAX_RANK = 0x00900000, SEA_ATTACK = 0x00900001, INEXHAUSTIBLE_FISH = 0x00900002, MORE_ANIMALS = 0x00900003, BURN_DURATION = 0x00900004, NO_ALLIED_PUSH = 0x00900005, diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index dfbb2ca1c4..1750ca60ad 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -11,6 +11,9 @@ #include "network/GameClient.h" #include "ogl/glArchivItem_Bitmap_Player.h" #include "world/GameWorld.h" +#include "gameTypes/Resource.h" +#include "gameData/GameConsts.h" +#include "random/Random.h" nofMiner::nofMiner(const MapPoint pos, const unsigned char player, nobUsual* workplace) : nofWorkman(Job::Miner, pos, player, workplace) @@ -69,32 +72,63 @@ helpers::OptionalEnum nofMiner::ProduceWare() } } +MapPoint nofMiner::FindPointWithResourceQuiet(ResourceType type) const +{ + const auto pts = world->GetMatchingPointsInRadius<1>( + pos, MINER_RADIUS, [this, type](const MapPoint pt) { return world->GetNode(pt).resources.has(type); }, true); + return pts.empty() ? MapPoint::Invalid() : pts.front(); +} + +bool nofMiner::CanCreateWorkEverywhereGraniteResource() const +{ + return workplace->GetBuildingType() == BuildingType::GraniteMine + && world->GetGGS().isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE) + && world->GetNode(pos).resources.getType() == ResourceType::Nothing; +} + +MapPoint nofMiner::CreateWorkEverywhereGraniteResource() +{ + if(!CanCreateWorkEverywhereGraniteResource()) + return MapPoint::Invalid(); + + world->SetResource(pos, Resource(ResourceType::Granite, static_cast(8 + RANDOM_RAND(8)))); + return pos; +} + bool nofMiner::AreWaresAvailable() const { - return nofWorkman::AreWaresAvailable() - && (CanMineWithoutResource() || FindPointWithResource(GetRequiredResType()).isValid()); + if(!nofWorkman::AreWaresAvailable()) + return false; + + if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() || CanCreateWorkEverywhereGraniteResource()) + return true; + + workplace->OnOutOfResources(); + return false; } bool nofMiner::StartWorking() { const GlobalGameSettings& settings = world->GetGGS(); - if(!CanMineWithoutResource()) + MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); + if(!resPt.isValid()) { - MapPoint resPt = FindPointWithResource(GetRequiredResType()); + resPt = CreateWorkEverywhereGraniteResource(); if(!resPt.isValid()) + { + workplace->OnOutOfResources(); return false; - if(!settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) - world->ReduceResource(resPt); + } } - return nofWorkman::StartWorking(); -} -bool nofMiner::CanMineWithoutResource() const -{ - return workplace->GetBuildingType() == BuildingType::GraniteMine - && world->GetGGS().isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES); -} + const bool inexhaustibleRes = settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES) + || (workplace->GetBuildingType() == BuildingType::GraniteMine + && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)); + if(!inexhaustibleRes) + world->ReduceResource(resPt); + return nofWorkman::StartWorking(); +} ResourceType nofMiner::GetRequiredResType() const { switch(workplace->GetBuildingType()) diff --git a/libs/s25main/figures/nofMiner.h b/libs/s25main/figures/nofMiner.h index d965a0fdcd..b45fa765ea 100644 --- a/libs/s25main/figures/nofMiner.h +++ b/libs/s25main/figures/nofMiner.h @@ -23,7 +23,9 @@ class nofMiner : public nofWorkman bool AreWaresAvailable() const override; bool StartWorking() override; ResourceType GetRequiredResType() const; - bool CanMineWithoutResource() const; + MapPoint FindPointWithResourceQuiet(ResourceType type) const; + bool CanCreateWorkEverywhereGraniteResource() const; + MapPoint CreateWorkEverywhereGraniteResource(); public: nofMiner(MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 9783f82a53..ba54e84ce1 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -26,10 +26,10 @@ BOOST_AUTO_TEST_SUITE(Production) namespace { struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P { - const nobUsual* CreateGraniteMineWithoutResources() + MapPoint CreateGraniteMineWithoutResources() { GoodsAndPeopleCounts inv; - inv[GoodType::Fish] = 10; + inv[GoodType::Fish] = 40; inv[GoodType::PickAxe] = 1; inv[Job::Miner] = 1; world.GetSpecObj(hqPos)->AddToInventory(inv, true); @@ -39,7 +39,7 @@ struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans)); BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, std::vector(2, Direction::West)); RTTR_EXEC_TILL(500, mine->HasWorker()); - return mine; + return minePos; } }; } // namespace @@ -134,14 +134,50 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithou BOOST_TEST(curInventory[GoodType::Stones] == initialStones); } -BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineWorksWithoutResources, GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteMineWithoutResourcesFixture) { ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; + RTTR_SKIP_GFS(2000); + + BOOST_TEST(curInventory[GoodType::Stones] == initialStones); +} + +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereCreatesDepletableResource, GraniteMineWithoutResourcesFixture) +{ + ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); + const MapPoint minePos = CreateGraniteMineWithoutResources(); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialStones = curInventory[GoodType::Stones]; + + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); + + RTTR_EXEC_TILL(50000, world.GetNode(minePos).resources.getType() == ResourceType::Granite + && world.GetNode(minePos).resources.getAmount() == 0u); + BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) == static_cast(ResourceType::Granite)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); +} + +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereResourceIsInexhaustibleWithGraniteAddon, GraniteMineWithoutResourcesFixture) +{ + ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); + const MapPoint minePos = CreateGraniteMineWithoutResources(); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialStones = curInventory[GoodType::Stones]; + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); + const unsigned initialResourceAmount = world.GetNode(minePos).resources.getAmount(); + + RTTR_SKIP_GFS(10000); + + BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == initialResourceAmount); } BOOST_AUTO_TEST_SUITE_END() From 44bbab07905db283fd64b8d191db566f723db8cf Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 14 May 2026 20:38:57 +0200 Subject: [PATCH 07/23] Implement configurable mine resource behavior --- libs/s25main/GlobalGameSettings.cpp | 4 + .../addons/AddonMineResourceBehavior.h | 57 ++++++ libs/s25main/addons/Addons.h | 3 +- libs/s25main/addons/const_addons.h | 2 + libs/s25main/figures/nofMiner.cpp | 171 +++++++++++++++--- libs/s25main/figures/nofMiner.h | 2 - libs/s25main/gameTypes/MineResourceBehavior.h | 13 ++ tests/s25Main/integration/testProduction.cpp | 111 +++++++++++- 8 files changed, 333 insertions(+), 30 deletions(-) create mode 100644 libs/s25main/addons/AddonMineResourceBehavior.h create mode 100644 libs/s25main/gameTypes/MineResourceBehavior.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index c855b37418..e35939d70a 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -80,6 +80,10 @@ void GlobalGameSettings::registerAllAddons() AddonInexhaustibleFish, AddonInexhaustibleGraniteMines, AddonGraniteMinesWorkEverywhere, + AddonCoalMineResourceBehavior, + AddonIronMineResourceBehavior, + AddonGoldMineResourceBehavior, + AddonGraniteMineResourceBehavior, AddonInexhaustibleMines, AddonLimitCatapults, AddonManualRoadEnlargement, diff --git a/libs/s25main/addons/AddonMineResourceBehavior.h b/libs/s25main/addons/AddonMineResourceBehavior.h new file mode 100644 index 0000000000..e9a443f168 --- /dev/null +++ b/libs/s25main/addons/AddonMineResourceBehavior.h @@ -0,0 +1,57 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonList.h" +#include "const_addons.h" +#include "mygettext/mygettext.h" +#include "gameTypes/MineResourceBehavior.h" +#include + +class AddonMineResourceBehaviorBase : public AddonList +{ +protected: + AddonMineResourceBehaviorBase(AddonId id, const std::string& name, const std::string& description) + : AddonList(id, AddonGroup::Economy, name, description, + {_("Default"), _("S4-like exhaustion"), _("Inexhaustible"), _("Work everywhere")}, + static_cast(MineResourceBehavior::Default)) + {} +}; + +class AddonCoalMineResourceBehavior : public AddonMineResourceBehaviorBase +{ +public: + AddonCoalMineResourceBehavior() + : AddonMineResourceBehaviorBase(AddonId::COALMINE_RESOURCE_BEHAVIOR, _("Coal Mine Resource Behavior"), + _("Configures how coal mines consume and exhaust coal deposits.")) + {} +}; + +class AddonIronMineResourceBehavior : public AddonMineResourceBehaviorBase +{ +public: + AddonIronMineResourceBehavior() + : AddonMineResourceBehaviorBase(AddonId::IRONMINE_RESOURCE_BEHAVIOR, _("Iron Mine Resource Behavior"), + _("Configures how iron mines consume and exhaust iron deposits.")) + {} +}; + +class AddonGoldMineResourceBehavior : public AddonMineResourceBehaviorBase +{ +public: + AddonGoldMineResourceBehavior() + : AddonMineResourceBehaviorBase(AddonId::GOLDMINE_RESOURCE_BEHAVIOR, _("Gold Mine Resource Behavior"), + _("Configures how gold mines consume and exhaust gold deposits.")) + {} +}; + +class AddonGraniteMineResourceBehavior : public AddonMineResourceBehaviorBase +{ +public: + AddonGraniteMineResourceBehavior() + : AddonMineResourceBehaviorBase(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, _("Granite Mine Resource Behavior"), + _("Configures how granite mines consume and exhaust stone deposits.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index ac7f84bf61..3a7da75e6b 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -33,11 +33,12 @@ #include "addons/AddonToolOrdering.h" +#include "addons/AddonGraniteMinesWorkEverywhere.h" #include "addons/AddonInexhaustibleFish.h" #include "addons/AddonInexhaustibleGraniteMines.h" -#include "addons/AddonGraniteMinesWorkEverywhere.h" #include "addons/AddonMaxRank.h" #include "addons/AddonMilitaryAid.h" +#include "addons/AddonMineResourceBehavior.h" #include "addons/AddonSeaAttack.h" #include "addons/AddonBattlefieldPromotion.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index c031b36218..bd65bfbef0 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -56,6 +56,8 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, INEXHAUSTIBLE_GRANITEMINES = 0x00800000, GRANITEMINES_WORK_EVERYWHERE = 0x00800001, + COALMINE_RESOURCE_BEHAVIOR = 0x00800002, IRONMINE_RESOURCE_BEHAVIOR = 0x00800003, + GOLDMINE_RESOURCE_BEHAVIOR = 0x00800004, GRANITEMINE_RESOURCE_BEHAVIOR = 0x00800005, MAX_RANK = 0x00900000, SEA_ATTACK = 0x00900001, INEXHAUSTIBLE_FISH = 0x00900002, MORE_ANIMALS = 0x00900003, BURN_DURATION = 0x00900004, NO_ALLIED_PUSH = 0x00900005, diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 1750ca60ad..856928265a 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -10,10 +10,120 @@ #include "buildings/nobUsual.h" #include "network/GameClient.h" #include "ogl/glArchivItem_Bitmap_Player.h" +#include "random/Random.h" #include "world/GameWorld.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameTypes/Resource.h" #include "gameData/GameConsts.h" -#include "random/Random.h" +#include +#include + +namespace { +constexpr unsigned MAX_PRODUCTION_PERCENT = 100; +constexpr unsigned S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE = 5; +constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; +constexpr uint8_t WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT = 8; +constexpr unsigned WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS = 8; + +AddonId GetResourceBehaviorAddonId(const BuildingType buildingType) +{ + switch(buildingType) + { + case BuildingType::GoldMine: return AddonId::GOLDMINE_RESOURCE_BEHAVIOR; + case BuildingType::IronMine: return AddonId::IRONMINE_RESOURCE_BEHAVIOR; + case BuildingType::CoalMine: return AddonId::COALMINE_RESOURCE_BEHAVIOR; + default: return AddonId::GRANITEMINE_RESOURCE_BEHAVIOR; + } +} + +MineResourceBehavior GetConfiguredResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) +{ + switch(static_cast(settings.getSelection(GetResourceBehaviorAddonId(buildingType)))) + { + case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; + case MineResourceBehavior::Inexhaustible: return MineResourceBehavior::Inexhaustible; + case MineResourceBehavior::WorkEverywhere: return MineResourceBehavior::WorkEverywhere; + default: return MineResourceBehavior::Default; + } +} + +MineResourceBehavior GetEffectiveResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType, + const MineResourceBehavior configuredBehavior) +{ + if(configuredBehavior != MineResourceBehavior::Default) + return configuredBehavior; + + if(buildingType == BuildingType::GraniteMine && settings.isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE)) + return MineResourceBehavior::WorkEverywhere; + + if(settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) + return MineResourceBehavior::Inexhaustible; + + return MineResourceBehavior::Default; +} + +bool ShouldReduceResources(const GlobalGameSettings& settings, const BuildingType buildingType, + const MineResourceBehavior configuredBehavior, const MineResourceBehavior effectiveBehavior) +{ + if(effectiveBehavior == MineResourceBehavior::Inexhaustible) + return false; + + if(configuredBehavior == MineResourceBehavior::Default && settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) + return false; + + if(configuredBehavior == MineResourceBehavior::Default && buildingType == BuildingType::GraniteMine + && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)) + return false; + + return true; +} + +unsigned GetS4LikeProductionChance(const GameWorld& world, const std::vector& resourcePts) +{ + unsigned resourceAmount = 0; + for(const MapPoint pt : resourcePts) + resourceAmount += world.GetNode(pt).resources.getAmount(); + + return std::min(MAX_PRODUCTION_PERCENT, resourceAmount * S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE); +} + +std::vector GetPointsWithResource(const GameWorld& world, const MapPoint pos, const ResourceType type) +{ + return world.GetMatchingPointsInRadius<1>( + pos, MINER_RADIUS, [&world, type](const MapPoint pt) { return world.GetNode(pt).resources.has(type); }, true); +} + +bool CanCreateWorkEverywhereResource(const GameWorld& world, const MapPoint pos, const MineResourceBehavior behavior) +{ + return behavior == MineResourceBehavior::WorkEverywhere + && world.GetNode(pos).resources.getType() == ResourceType::Nothing; +} + +MapPoint CreateWorkEverywhereResource(GameWorld& world, const MapPoint pos, const ResourceType type, + const MineResourceBehavior behavior, const unsigned objId) +{ + if(!CanCreateWorkEverywhereResource(world, pos, behavior)) + return MapPoint::Invalid(); + + const auto amount = + static_cast(WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT + + RANDOM.Rand(RANDOM_CONTEXT2(objId), WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS)); + world.SetResource(pos, Resource(type, amount)); + return pos; +} + +void ReduceS4LikeResource(GameWorld& world, const std::vector& resourcePts) +{ + for(const MapPoint pt : resourcePts) + { + if(world.GetNode(pt).resources.getAmount() > S4LIKE_MIN_RESOURCE_AMOUNT) + { + world.ReduceResource(pt); + return; + } + } +} +} // namespace nofMiner::nofMiner(const MapPoint pos, const unsigned char player, nobUsual* workplace) : nofWorkman(Job::Miner, pos, player, workplace) @@ -63,6 +173,25 @@ unsigned short nofMiner::GetCarryID() const helpers::OptionalEnum nofMiner::ProduceWare() { + const GlobalGameSettings& settings = world->GetGGS(); + const MineResourceBehavior configuredBehavior = + GetConfiguredResourceBehavior(settings, workplace->GetBuildingType()); + const MineResourceBehavior effectiveBehavior = + GetEffectiveResourceBehavior(settings, workplace->GetBuildingType(), configuredBehavior); + + if(effectiveBehavior == MineResourceBehavior::S4LikeExhaustion) + { + const std::vector resourcePts = GetPointsWithResource(*world, pos, GetRequiredResType()); + const auto productionRoll = static_cast(RANDOM_RAND(MAX_PRODUCTION_PERCENT)); + const bool produceNothingThisCycle = + resourcePts.empty() || productionRoll >= GetS4LikeProductionChance(*world, resourcePts); + if(produceNothingThisCycle) + return boost::none; + + if(ShouldReduceResources(settings, workplace->GetBuildingType(), configuredBehavior, effectiveBehavior)) + ReduceS4LikeResource(*world, resourcePts); + } + switch(workplace->GetBuildingType()) { case BuildingType::GoldMine: return GoodType::Gold; @@ -74,33 +203,22 @@ helpers::OptionalEnum nofMiner::ProduceWare() MapPoint nofMiner::FindPointWithResourceQuiet(ResourceType type) const { - const auto pts = world->GetMatchingPointsInRadius<1>( - pos, MINER_RADIUS, [this, type](const MapPoint pt) { return world->GetNode(pt).resources.has(type); }, true); + const auto pts = GetPointsWithResource(*world, pos, type); return pts.empty() ? MapPoint::Invalid() : pts.front(); } -bool nofMiner::CanCreateWorkEverywhereGraniteResource() const -{ - return workplace->GetBuildingType() == BuildingType::GraniteMine - && world->GetGGS().isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE) - && world->GetNode(pos).resources.getType() == ResourceType::Nothing; -} - -MapPoint nofMiner::CreateWorkEverywhereGraniteResource() -{ - if(!CanCreateWorkEverywhereGraniteResource()) - return MapPoint::Invalid(); - - world->SetResource(pos, Resource(ResourceType::Granite, static_cast(8 + RANDOM_RAND(8)))); - return pos; -} - bool nofMiner::AreWaresAvailable() const { if(!nofWorkman::AreWaresAvailable()) return false; - if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() || CanCreateWorkEverywhereGraniteResource()) + const MineResourceBehavior configuredBehavior = + GetConfiguredResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); + const MineResourceBehavior effectiveBehavior = + GetEffectiveResourceBehavior(world->GetGGS(), workplace->GetBuildingType(), configuredBehavior); + + if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() + || CanCreateWorkEverywhereResource(*world, pos, effectiveBehavior)) return true; workplace->OnOutOfResources(); @@ -110,10 +228,14 @@ bool nofMiner::AreWaresAvailable() const bool nofMiner::StartWorking() { const GlobalGameSettings& settings = world->GetGGS(); + const MineResourceBehavior configuredBehavior = + GetConfiguredResourceBehavior(settings, workplace->GetBuildingType()); + const MineResourceBehavior effectiveBehavior = + GetEffectiveResourceBehavior(settings, workplace->GetBuildingType(), configuredBehavior); MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); if(!resPt.isValid()) { - resPt = CreateWorkEverywhereGraniteResource(); + resPt = CreateWorkEverywhereResource(*world, pos, GetRequiredResType(), effectiveBehavior, GetObjId()); if(!resPt.isValid()) { workplace->OnOutOfResources(); @@ -121,14 +243,13 @@ bool nofMiner::StartWorking() } } - const bool inexhaustibleRes = settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES) - || (workplace->GetBuildingType() == BuildingType::GraniteMine - && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)); - if(!inexhaustibleRes) + if(effectiveBehavior != MineResourceBehavior::S4LikeExhaustion + && ShouldReduceResources(settings, workplace->GetBuildingType(), configuredBehavior, effectiveBehavior)) world->ReduceResource(resPt); return nofWorkman::StartWorking(); } + ResourceType nofMiner::GetRequiredResType() const { switch(workplace->GetBuildingType()) diff --git a/libs/s25main/figures/nofMiner.h b/libs/s25main/figures/nofMiner.h index b45fa765ea..b13cf9a736 100644 --- a/libs/s25main/figures/nofMiner.h +++ b/libs/s25main/figures/nofMiner.h @@ -24,8 +24,6 @@ class nofMiner : public nofWorkman bool StartWorking() override; ResourceType GetRequiredResType() const; MapPoint FindPointWithResourceQuiet(ResourceType type) const; - bool CanCreateWorkEverywhereGraniteResource() const; - MapPoint CreateWorkEverywhereGraniteResource(); public: nofMiner(MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h new file mode 100644 index 0000000000..31bd4bf19f --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -0,0 +1,13 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +enum class MineResourceBehavior : unsigned +{ + Default, + S4LikeExhaustion, + Inexhaustible, + WorkEverywhere +}; diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index ba54e84ce1..5910ce8096 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -7,7 +7,9 @@ #include "factories/BuildingFactory.h" #include "postSystem/PostBox.h" #include "postSystem/PostMsg.h" +#include "random/Random.h" #include "worldFixtures/WorldWithGCExecution.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameData/ToolConsts.h" #include #include @@ -24,6 +26,17 @@ static std::ostream& operator<<(std::ostream& os, const PostCategory& cat) BOOST_AUTO_TEST_SUITE(Production) namespace { +GoodType GetMineGoodType(const BuildingType mineType) +{ + switch(mineType) + { + case BuildingType::GoldMine: return GoodType::Gold; + case BuildingType::IronMine: return GoodType::IronOre; + case BuildingType::CoalMine: return GoodType::Coal; + default: return GoodType::Stones; + } +} + struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P { MapPoint CreateGraniteMineWithoutResources() @@ -42,6 +55,44 @@ struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P return minePos; } }; + +struct MineProductionFixture : WorldWithGCExecution1P +{ + void AddMinerSupplies() + { + GoodsAndPeopleCounts inv; + inv[GoodType::Fish] = 40; + inv[GoodType::PickAxe] = 1; + inv[Job::Miner] = 1; + world.GetSpecObj(hqPos)->AddToInventory(inv, true); + } + + const nobUsual* PlaceMine(const BuildingType mineType, MapPoint& minePos) + { + minePos = hqPos + MapPoint(2, 0); + return static_cast( + BuildingFactory::CreateBuilding(world, mineType, minePos, curPlayer, Nation::Romans)); + } + + void ConnectMineAndWaitForWorker(const MapPoint minePos, const nobUsual* mine) + { + BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, std::vector(2, Direction::West)); + RTTR_EXEC_TILL(500, mine->HasWorker()); + } + + MapPoint CreateMine(const BuildingType mineType, const Resource initialResource = Resource()) + { + AddMinerSupplies(); + MapPoint minePos; + const nobUsual* mine = PlaceMine(mineType, minePos); + if(initialResource.getType() != ResourceType::Nothing) + world.GetNodeWriteable(minePos).resources = initialResource; + ConnectMineAndWaitForWorker(minePos, mine); + return minePos; + } + + void ResetMineProductionRng(const unsigned seed) { RANDOM.Init(seed); } +}; } // namespace BOOST_FIXTURE_TEST_CASE(MetalWorkerStopped, WorldWithGCExecution1P) @@ -158,11 +209,13 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereCreatesDepletableResource, Gran RTTR_EXEC_TILL(50000, world.GetNode(minePos).resources.getType() == ResourceType::Granite && world.GetNode(minePos).resources.getAmount() == 0u); - BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) == static_cast(ResourceType::Granite)); + BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) + == static_cast(ResourceType::Granite)); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereResourceIsInexhaustibleWithGraniteAddon, GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereResourceIsInexhaustibleWithGraniteAddon, + GraniteMineWithoutResourcesFixture) { ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); @@ -180,4 +233,58 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereResourceIsInexhaustibleWithGran BOOST_TEST(world.GetNode(minePos).resources.getAmount() == initialResourceAmount); } +BOOST_FIXTURE_TEST_CASE(CoalMineInexhaustibleBehaviorDoesNotDepleteResource, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 4)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GetMineGoodType(BuildingType::CoalMine)]; + + RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 4u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionCanProduceNothing, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + + ResetMineProductionRng(2); + RTTR_SKIP_GFS(2000); + + BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionReducesResourceOnSuccessfulCycle, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 15)); + + ResetMineProductionRng(21); + RTTR_EXEC_TILL(5000, world.GetNode(minePos).resources.getAmount() == 14u); + + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 14u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorCreatesDepletableResource, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + + RTTR_EXEC_TILL(2000, curInventory[GoodType::Coal] > initialCoal); + BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Coal)); + + RTTR_EXEC_TILL(50000, world.GetNode(minePos).resources.getType() == ResourceType::Coal + && world.GetNode(minePos).resources.getAmount() == 0u); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); +} + BOOST_AUTO_TEST_SUITE_END() From 64a8b7130fab5f5c2e7805c07d1ca9dae3063833 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 15 May 2026 23:40:02 +0200 Subject: [PATCH 08/23] Implement mine no-output fallback behavior --- libs/s25main/GlobalGameSettings.cpp | 1 + .../addons/AddonMineNoOutputFallback.h | 22 ++++ libs/s25main/addons/Addons.h | 1 + libs/s25main/addons/const_addons.h | 1 + libs/s25main/figures/nofMiner.cpp | 60 ++++++++++- libs/s25main/gameTypes/MineNoOutputFallback.h | 14 +++ tests/s25Main/integration/testProduction.cpp | 101 ++++++++++++++++++ 7 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 libs/s25main/addons/AddonMineNoOutputFallback.h create mode 100644 libs/s25main/gameTypes/MineNoOutputFallback.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index e35939d70a..5ac5c8297b 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -84,6 +84,7 @@ void GlobalGameSettings::registerAllAddons() AddonIronMineResourceBehavior, AddonGoldMineResourceBehavior, AddonGraniteMineResourceBehavior, + AddonMineNoOutputFallback, AddonInexhaustibleMines, AddonLimitCatapults, AddonManualRoadEnlargement, diff --git a/libs/s25main/addons/AddonMineNoOutputFallback.h b/libs/s25main/addons/AddonMineNoOutputFallback.h new file mode 100644 index 0000000000..be8536f713 --- /dev/null +++ b/libs/s25main/addons/AddonMineNoOutputFallback.h @@ -0,0 +1,22 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "AddonList.h" +#include "const_addons.h" +#include "mygettext/mygettext.h" +#include "gameTypes/MineNoOutputFallback.h" + +class AddonMineNoOutputFallback : public AddonList +{ +public: + AddonMineNoOutputFallback() + : AddonList(AddonId::MINE_NO_OUTPUT_FALLBACK, AddonGroup::Economy, _("Mine No-Output Fallback"), + _("Configures what mines produce when S4-like exhaustion would produce nothing."), + {_("Produce nothing"), _("Produce granite 25%"), _("Produce granite 50%"), + _("Produce granite 100%"), _("Produce lower grade resource")}, + static_cast(MineNoOutputFallback::ProduceNothing)) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 3a7da75e6b..5bc3a81d73 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -38,6 +38,7 @@ #include "addons/AddonInexhaustibleGraniteMines.h" #include "addons/AddonMaxRank.h" #include "addons/AddonMilitaryAid.h" +#include "addons/AddonMineNoOutputFallback.h" #include "addons/AddonMineResourceBehavior.h" #include "addons/AddonSeaAttack.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index bd65bfbef0..f18cd65126 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -58,6 +58,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x INEXHAUSTIBLE_GRANITEMINES = 0x00800000, GRANITEMINES_WORK_EVERYWHERE = 0x00800001, COALMINE_RESOURCE_BEHAVIOR = 0x00800002, IRONMINE_RESOURCE_BEHAVIOR = 0x00800003, GOLDMINE_RESOURCE_BEHAVIOR = 0x00800004, GRANITEMINE_RESOURCE_BEHAVIOR = 0x00800005, + MINE_NO_OUTPUT_FALLBACK = 0x00800006, MAX_RANK = 0x00900000, SEA_ATTACK = 0x00900001, INEXHAUSTIBLE_FISH = 0x00900002, MORE_ANIMALS = 0x00900003, BURN_DURATION = 0x00900004, NO_ALLIED_PUSH = 0x00900005, diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 856928265a..f4b24ac013 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -12,6 +12,7 @@ #include "ogl/glArchivItem_Bitmap_Player.h" #include "random/Random.h" #include "world/GameWorld.h" +#include "gameTypes/MineNoOutputFallback.h" #include "gameTypes/MineResourceBehavior.h" #include "gameTypes/Resource.h" #include "gameData/GameConsts.h" @@ -20,6 +21,8 @@ namespace { constexpr unsigned MAX_PRODUCTION_PERCENT = 100; +constexpr unsigned GRANITE_FALLBACK_25_PERCENT = 25; +constexpr unsigned GRANITE_FALLBACK_50_PERCENT = 50; constexpr unsigned S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE = 5; constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; constexpr uint8_t WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT = 8; @@ -36,6 +39,18 @@ AddonId GetResourceBehaviorAddonId(const BuildingType buildingType) } } +MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& settings) +{ + switch(static_cast(settings.getSelection(AddonId::MINE_NO_OUTPUT_FALLBACK))) + { + case MineNoOutputFallback::ProduceGranite25: return MineNoOutputFallback::ProduceGranite25; + case MineNoOutputFallback::ProduceGranite50: return MineNoOutputFallback::ProduceGranite50; + case MineNoOutputFallback::ProduceGranite100: return MineNoOutputFallback::ProduceGranite100; + case MineNoOutputFallback::ProduceLowerGradeResource: return MineNoOutputFallback::ProduceLowerGradeResource; + default: return MineNoOutputFallback::ProduceNothing; + } +} + MineResourceBehavior GetConfiguredResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) { switch(static_cast(settings.getSelection(GetResourceBehaviorAddonId(buildingType)))) @@ -87,6 +102,49 @@ unsigned GetS4LikeProductionChance(const GameWorld& world, const std::vector GetLowerGradeFallbackGood(const BuildingType buildingType) +{ + switch(buildingType) + { + case BuildingType::GoldMine: return GoodType::IronOre; + case BuildingType::IronMine: return GoodType::Coal; + case BuildingType::CoalMine: return GoodType::Stones; + default: return boost::none; + } +} + +helpers::OptionalEnum GetNoOutputFallbackGood(const GlobalGameSettings& settings, + const BuildingType buildingType, const unsigned objId) +{ + const MineNoOutputFallback fallback = GetConfiguredNoOutputFallback(settings); + const unsigned graniteFallbackChance = GetGraniteFallbackChance(fallback); + if(graniteFallbackChance > 0) + { + if(graniteFallbackChance == MAX_PRODUCTION_PERCENT + || static_cast(RANDOM.Rand(RANDOM_CONTEXT2(objId), MAX_PRODUCTION_PERCENT)) + < graniteFallbackChance) + return GoodType::Stones; + + return boost::none; + } + + if(fallback == MineNoOutputFallback::ProduceLowerGradeResource) + return GetLowerGradeFallbackGood(buildingType); + + return boost::none; +} + std::vector GetPointsWithResource(const GameWorld& world, const MapPoint pos, const ResourceType type) { return world.GetMatchingPointsInRadius<1>( @@ -186,7 +244,7 @@ helpers::OptionalEnum nofMiner::ProduceWare() const bool produceNothingThisCycle = resourcePts.empty() || productionRoll >= GetS4LikeProductionChance(*world, resourcePts); if(produceNothingThisCycle) - return boost::none; + return GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); if(ShouldReduceResources(settings, workplace->GetBuildingType(), configuredBehavior, effectiveBehavior)) ReduceS4LikeResource(*world, resourcePts); diff --git a/libs/s25main/gameTypes/MineNoOutputFallback.h b/libs/s25main/gameTypes/MineNoOutputFallback.h new file mode 100644 index 0000000000..7d0d9dc966 --- /dev/null +++ b/libs/s25main/gameTypes/MineNoOutputFallback.h @@ -0,0 +1,14 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +enum class MineNoOutputFallback : unsigned +{ + ProduceNothing, + ProduceGranite25, + ProduceGranite50, + ProduceGranite100, + ProduceLowerGradeResource +}; diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 5910ce8096..df6a8b3c89 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -9,6 +9,7 @@ #include "postSystem/PostMsg.h" #include "random/Random.h" #include "worldFixtures/WorldWithGCExecution.h" +#include "gameTypes/MineNoOutputFallback.h" #include "gameTypes/MineResourceBehavior.h" #include "gameData/ToolConsts.h" #include @@ -260,6 +261,75 @@ BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionCanProduceNothing, MineProductio BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); } +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback25ProducesStones, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite25)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + const unsigned initialStones = curInventory[GoodType::Stones]; + + ResetMineProductionRng(2); + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + + BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback50ProducesStones, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite50)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + const unsigned initialStones = curInventory[GoodType::Stones]; + + ResetMineProductionRng(7); + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + + BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback100ProducesStones, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite100)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + const unsigned initialStones = curInventory[GoodType::Stones]; + + ResetMineProductionRng(2); + RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + + BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); +} + +BOOST_FIXTURE_TEST_CASE(GoldMineS4LikeNoOutputLowerGradeFallbackProducesIronOre, MineProductionFixture) +{ + ggs.setSelection(AddonId::GOLDMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, + static_cast(MineNoOutputFallback::ProduceLowerGradeResource)); + const MapPoint minePos = CreateMine(BuildingType::GoldMine, Resource(ResourceType::Gold, 1)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialGold = curInventory[GoodType::Gold]; + const unsigned initialIronOre = curInventory[GoodType::IronOre]; + + ResetMineProductionRng(2); + RTTR_EXEC_TILL(2000, curInventory[GoodType::IronOre] > initialIronOre); + + BOOST_TEST(curInventory[GoodType::Gold] == initialGold); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); +} + BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionReducesResourceOnSuccessfulCycle, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, @@ -272,6 +342,37 @@ BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionReducesResourceOnSuccessfulCycle BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 14u); } +BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeSuccessfulCycleIgnoresNoOutputFallback, MineProductionFixture) +{ + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite100)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 15)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + const unsigned initialStones = curInventory[GoodType::Stones]; + + ResetMineProductionRng(21); + RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + + BOOST_TEST(curInventory[GoodType::Stones] == initialStones); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 14u); +} + +BOOST_FIXTURE_TEST_CASE(CoalMineDefaultProductionIgnoresNoOutputFallback, MineProductionFixture) +{ + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite100)); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 3)); + const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); + const unsigned initialCoal = curInventory[GoodType::Coal]; + const unsigned initialStones = curInventory[GoodType::Stones]; + + RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + + BOOST_TEST(curInventory[GoodType::Stones] == initialStones); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() < 3u); +} + BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorCreatesDepletableResource, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); From 621d52e7bbd1c4a0cf03ed8ae24e632e825896a2 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 16 May 2026 00:15:15 +0200 Subject: [PATCH 09/23] Teach AI mine planning configurable resource behavior --- libs/s25main/ai/AIInterface.cpp | 45 ++++++++++ libs/s25main/ai/AIInterface.h | 2 + libs/s25main/ai/aijh/AIPlayerJH.cpp | 10 +-- libs/s25main/figures/nofMiner.cpp | 69 ++------------ .../gameTypes/MineResourceBehavior.cpp | 65 ++++++++++++++ libs/s25main/gameTypes/MineResourceBehavior.h | 9 ++ tests/s25Main/integration/testAI.cpp | 90 +++++++++++++++++++ 7 files changed, 221 insertions(+), 69 deletions(-) create mode 100644 libs/s25main/gameTypes/MineResourceBehavior.cpp diff --git a/libs/s25main/ai/AIInterface.cpp b/libs/s25main/ai/AIInterface.cpp index 8a2a59f042..1f0104a852 100644 --- a/libs/s25main/ai/AIInterface.cpp +++ b/libs/s25main/ai/AIInterface.cpp @@ -17,6 +17,7 @@ #include "pathfinding/RoadPathFinder.h" #include "nodeObjs/noFlag.h" #include "nodeObjs/noTree.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameData/TerrainDesc.h" #include #include @@ -45,6 +46,26 @@ bool IsPointOK_RoadPathEvenStep(const GameWorldBase& gwb, const MapPoint pt, con const auto* prp = static_cast(param); return prp->boat_road || gwb.GetBQ(pt, gwb.GetNode(pt).owner - 1) != BuildingQuality::Nothing; } + +helpers::OptionalEnum GetMineBuildingType(const AIResource res) +{ + switch(res) + { + case AIResource::Gold: return BuildingType::GoldMine; + case AIResource::Ironore: return BuildingType::IronMine; + case AIResource::Coal: return BuildingType::CoalMine; + case AIResource::Granite: return BuildingType::GraniteMine; + default: return boost::none; + } +} + +int GetS4LikeMineResourceRating(const Resource resource, const unsigned defaultRating) +{ + if(resource.getAmount() == 0u) + return 0; + + return std::max(1u, std::min(static_cast(resource.getAmount()), defaultRating)); +} } // namespace AIInterface::AIInterface(const GameWorldBase& gwb, std::vector& gcs, unsigned char playerID) @@ -158,6 +179,23 @@ int AIInterface::GetResourceRating(const MapPoint pt, AIResource res) const case AIResource::Ironore: case AIResource::Coal: case AIResource::Granite: + { + const Resource subres = gwb.GetNode(pt).resources; + if(convertToNodeResource(GetSubsurfaceResource(pt)) == res) + { + const auto mineBuildingType = GetMineBuildingType(res); + if(mineBuildingType + && GetEffectiveMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) + == MineResourceBehavior::S4LikeExhaustion) + return GetS4LikeMineResourceRating(subres, RES_RADIUS[res]); + + return RES_RADIUS[res]; + } + if(IsMineResourceWorkEverywhere(res) && subres.getType() == ResourceType::Nothing + && gwb.IsOfTerrain(pt, [](const TerrainDesc& desc) { return desc.Is(ETerrain::Mineable); })) + return RES_RADIUS[res]; + break; + } case AIResource::Fish: if(convertToNodeResource(GetSubsurfaceResource(pt)) == res) return RES_RADIUS[res]; @@ -166,6 +204,13 @@ int AIInterface::GetResourceRating(const MapPoint pt, AIResource res) const return 0; } +bool AIInterface::IsMineResourceWorkEverywhere(const AIResource res) const +{ + const auto mineBuildingType = GetMineBuildingType(res); + return mineBuildingType + && GetEffectiveMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) == MineResourceBehavior::WorkEverywhere; +} + int AIInterface::CalcResourceValue(const MapPoint pt, AIResource res, helpers::OptionalEnum direction, int lastval) const { diff --git a/libs/s25main/ai/AIInterface.h b/libs/s25main/ai/AIInterface.h index 763f4b39f5..0fefdb780c 100644 --- a/libs/s25main/ai/AIInterface.h +++ b/libs/s25main/ai/AIInterface.h @@ -52,6 +52,8 @@ class AIInterface : public GameCommandFactory int lastval = 0xffff) const; /// Calculate the resource value for a given point int GetResourceRating(MapPoint pt, AIResource res) const; + /// Check whether the given mine resource can be produced on otherwise empty mineable mountain. + bool IsMineResourceWorkEverywhere(AIResource res) const; /// Test whether a given point is part of the border or not bool IsBorder(const MapPoint pt) const { diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index 72fbee111f..7c7eb0d0c7 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -32,6 +32,7 @@ #include "nodeObjs/noFlag.h" #include "nodeObjs/noShip.h" #include "nodeObjs/noTree.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameData/BuildingConsts.h" #include "gameData/BuildingProperties.h" #include "gameData/GameConsts.h" @@ -139,11 +140,10 @@ static bool isUnlimitedResource(const AIResource res, const GlobalGameSettings& { switch(res) { - case AIResource::Gold: - case AIResource::Ironore: - case AIResource::Coal: return ggs.isEnabled(AddonId::INEXHAUSTIBLE_MINES); - case AIResource::Granite: - return ggs.isEnabled(AddonId::INEXHAUSTIBLE_MINES) || ggs.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES); + case AIResource::Gold: return !IsMineResourceDepletable(ggs, BuildingType::GoldMine); + case AIResource::Ironore: return !IsMineResourceDepletable(ggs, BuildingType::IronMine); + case AIResource::Coal: return !IsMineResourceDepletable(ggs, BuildingType::CoalMine); + case AIResource::Granite: return !IsMineResourceDepletable(ggs, BuildingType::GraniteMine); case AIResource::Fish: return ggs.isEnabled(AddonId::INEXHAUSTIBLE_FISH); default: return false; } diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index f4b24ac013..b7e3b47e4e 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -28,17 +28,6 @@ constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; constexpr uint8_t WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT = 8; constexpr unsigned WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS = 8; -AddonId GetResourceBehaviorAddonId(const BuildingType buildingType) -{ - switch(buildingType) - { - case BuildingType::GoldMine: return AddonId::GOLDMINE_RESOURCE_BEHAVIOR; - case BuildingType::IronMine: return AddonId::IRONMINE_RESOURCE_BEHAVIOR; - case BuildingType::CoalMine: return AddonId::COALMINE_RESOURCE_BEHAVIOR; - default: return AddonId::GRANITEMINE_RESOURCE_BEHAVIOR; - } -} - MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& settings) { switch(static_cast(settings.getSelection(AddonId::MINE_NO_OUTPUT_FALLBACK))) @@ -51,48 +40,6 @@ MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& set } } -MineResourceBehavior GetConfiguredResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) -{ - switch(static_cast(settings.getSelection(GetResourceBehaviorAddonId(buildingType)))) - { - case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; - case MineResourceBehavior::Inexhaustible: return MineResourceBehavior::Inexhaustible; - case MineResourceBehavior::WorkEverywhere: return MineResourceBehavior::WorkEverywhere; - default: return MineResourceBehavior::Default; - } -} - -MineResourceBehavior GetEffectiveResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType, - const MineResourceBehavior configuredBehavior) -{ - if(configuredBehavior != MineResourceBehavior::Default) - return configuredBehavior; - - if(buildingType == BuildingType::GraniteMine && settings.isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE)) - return MineResourceBehavior::WorkEverywhere; - - if(settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) - return MineResourceBehavior::Inexhaustible; - - return MineResourceBehavior::Default; -} - -bool ShouldReduceResources(const GlobalGameSettings& settings, const BuildingType buildingType, - const MineResourceBehavior configuredBehavior, const MineResourceBehavior effectiveBehavior) -{ - if(effectiveBehavior == MineResourceBehavior::Inexhaustible) - return false; - - if(configuredBehavior == MineResourceBehavior::Default && settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) - return false; - - if(configuredBehavior == MineResourceBehavior::Default && buildingType == BuildingType::GraniteMine - && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)) - return false; - - return true; -} - unsigned GetS4LikeProductionChance(const GameWorld& world, const std::vector& resourcePts) { unsigned resourceAmount = 0; @@ -232,10 +179,8 @@ unsigned short nofMiner::GetCarryID() const helpers::OptionalEnum nofMiner::ProduceWare() { const GlobalGameSettings& settings = world->GetGGS(); - const MineResourceBehavior configuredBehavior = - GetConfiguredResourceBehavior(settings, workplace->GetBuildingType()); const MineResourceBehavior effectiveBehavior = - GetEffectiveResourceBehavior(settings, workplace->GetBuildingType(), configuredBehavior); + GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); if(effectiveBehavior == MineResourceBehavior::S4LikeExhaustion) { @@ -246,7 +191,7 @@ helpers::OptionalEnum nofMiner::ProduceWare() if(produceNothingThisCycle) return GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); - if(ShouldReduceResources(settings, workplace->GetBuildingType(), configuredBehavior, effectiveBehavior)) + if(IsMineResourceDepletable(settings, workplace->GetBuildingType())) ReduceS4LikeResource(*world, resourcePts); } @@ -270,10 +215,8 @@ bool nofMiner::AreWaresAvailable() const if(!nofWorkman::AreWaresAvailable()) return false; - const MineResourceBehavior configuredBehavior = - GetConfiguredResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); const MineResourceBehavior effectiveBehavior = - GetEffectiveResourceBehavior(world->GetGGS(), workplace->GetBuildingType(), configuredBehavior); + GetEffectiveMineResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() || CanCreateWorkEverywhereResource(*world, pos, effectiveBehavior)) @@ -286,10 +229,8 @@ bool nofMiner::AreWaresAvailable() const bool nofMiner::StartWorking() { const GlobalGameSettings& settings = world->GetGGS(); - const MineResourceBehavior configuredBehavior = - GetConfiguredResourceBehavior(settings, workplace->GetBuildingType()); const MineResourceBehavior effectiveBehavior = - GetEffectiveResourceBehavior(settings, workplace->GetBuildingType(), configuredBehavior); + GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); if(!resPt.isValid()) { @@ -302,7 +243,7 @@ bool nofMiner::StartWorking() } if(effectiveBehavior != MineResourceBehavior::S4LikeExhaustion - && ShouldReduceResources(settings, workplace->GetBuildingType(), configuredBehavior, effectiveBehavior)) + && IsMineResourceDepletable(settings, workplace->GetBuildingType())) world->ReduceResource(resPt); return nofWorkman::StartWorking(); diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp new file mode 100644 index 0000000000..646646c057 --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -0,0 +1,65 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "MineResourceBehavior.h" +#include "GlobalGameSettings.h" +#include "addons/const_addons.h" +#include "gameTypes/BuildingType.h" + +AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) +{ + switch(buildingType) + { + case BuildingType::GoldMine: return AddonId::GOLDMINE_RESOURCE_BEHAVIOR; + case BuildingType::IronMine: return AddonId::IRONMINE_RESOURCE_BEHAVIOR; + case BuildingType::CoalMine: return AddonId::COALMINE_RESOURCE_BEHAVIOR; + default: return AddonId::GRANITEMINE_RESOURCE_BEHAVIOR; + } +} + +MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, + const BuildingType buildingType) +{ + switch(static_cast(settings.getSelection(GetMineResourceBehaviorAddonId(buildingType)))) + { + case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; + case MineResourceBehavior::Inexhaustible: return MineResourceBehavior::Inexhaustible; + case MineResourceBehavior::WorkEverywhere: return MineResourceBehavior::WorkEverywhere; + default: return MineResourceBehavior::Default; + } +} + +MineResourceBehavior GetEffectiveMineResourceBehavior(const GlobalGameSettings& settings, + const BuildingType buildingType) +{ + const MineResourceBehavior configuredBehavior = GetConfiguredMineResourceBehavior(settings, buildingType); + if(configuredBehavior != MineResourceBehavior::Default) + return configuredBehavior; + + if(buildingType == BuildingType::GraniteMine && settings.isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE)) + return MineResourceBehavior::WorkEverywhere; + + if(settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) + return MineResourceBehavior::Inexhaustible; + + return MineResourceBehavior::Default; +} + +bool IsMineResourceDepletable(const GlobalGameSettings& settings, const BuildingType buildingType) +{ + const MineResourceBehavior configuredBehavior = GetConfiguredMineResourceBehavior(settings, buildingType); + const MineResourceBehavior effectiveBehavior = GetEffectiveMineResourceBehavior(settings, buildingType); + + if(effectiveBehavior == MineResourceBehavior::Inexhaustible) + return false; + + if(configuredBehavior == MineResourceBehavior::Default && settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) + return false; + + if(configuredBehavior == MineResourceBehavior::Default && buildingType == BuildingType::GraniteMine + && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)) + return false; + + return true; +} diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index 31bd4bf19f..60c98ba0b7 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -4,6 +4,10 @@ #pragma once +class GlobalGameSettings; +enum class AddonId; +enum class BuildingType : unsigned char; + enum class MineResourceBehavior : unsigned { Default, @@ -11,3 +15,8 @@ enum class MineResourceBehavior : unsigned Inexhaustible, WorkEverywhere }; + +AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); +MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); +MineResourceBehavior GetEffectiveMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); +bool IsMineResourceDepletable(const GlobalGameSettings& settings, BuildingType buildingType); diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 3efb3fef97..358927f6a4 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -16,11 +16,15 @@ #include "network/GameMessage_Chat.h" #include "notifications/NodeNote.h" #include "worldFixtures/WorldWithGCExecution.h" +#include "worldFixtures/terrainHelpers.h" #include "nodeObjs/noFlag.h" #include "nodeObjs/noTree.h" #include "gameTypes/GameTypesOutput.h" +#include "gameTypes/MineResourceBehavior.h" +#include "gameTypes/Resource.h" #include "gameData/BuildingProperties.h" #include "gameData/MilitaryConsts.h" +#include "gameData/WorldDescription.h" #include "rttr/test/random.hpp" #include #include @@ -51,6 +55,37 @@ inline bool playerHasBld(const GamePlayer& player, BuildingType type) return !blds.GetBuildings(type).empty(); } +DescIdx GetMineableTerrain(const WorldDescription& desc) +{ + const auto terrain = desc.terrain.find([](const TerrainDesc& t) { return t.Is(ETerrain::Mineable); }); + BOOST_TEST_REQUIRE(terrain); + return terrain; +} + +void makeWorldMineable(GameWorld& world) +{ + const DescIdx mineableTerrain = GetMineableTerrain(world.GetDescription()); + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + MapNode& node = world.GetNodeWriteable(pt); + node.t1 = node.t2 = mineableTerrain; + node.resources = Resource(); + } + world.InitAfterLoad(); +} + +void makeMineNodesUsableForSearch(AIJH::AIPlayerJH& aijh, const GameWorld& world, const unsigned player) +{ + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + AIJH::Node& node = aijh.GetAINode(pt); + node.bq = world.GetBQ(pt, player); + node.owned = true; + node.reachable = true; + node.farmed = false; + } +} + struct MockAI final : public AIPlayer { MockAI(unsigned char playerId, const GameWorldBase& gwb, const AI::Level level) : AIPlayer(playerId, gwb, level) {} @@ -109,6 +144,61 @@ BOOST_FIXTURE_TEST_CASE(AIChat, EmptyWorldFixture2P) } } +BOOST_FIXTURE_TEST_CASE(MineResourceRatingAccountsForS4LikeExhaustion, EmptyWorldFixture1P) +{ + const MapPoint resourcePos = world.MakeMapPoint(world.GetPlayer(0).GetHQPos() + Position(2, 0)); + world.GetNodeWriteable(resourcePos).resources = Resource(ResourceType::Coal, 1); + + MockAI ai(0, world, AI::Level::Easy); + const int defaultRating = ai.getAIInterface().GetResourceRating(resourcePos, AIResource::Coal); + + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + const int s4LikeRating = ai.getAIInterface().GetResourceRating(resourcePos, AIResource::Coal); + + BOOST_TEST(defaultRating == static_cast(RES_RADIUS[AIResource::Coal])); + BOOST_TEST(s4LikeRating > 0); + BOOST_TEST(s4LikeRating < defaultRating); +} + +BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingAIResourceOnly, EmptyWorldFixture1P) +{ + makeWorldMineable(world); + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); + + AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); + makeMineNodesUsableForSearch(ai, world, 0); + + const MapPoint around = world.GetPlayer(0).GetHQPos(); + BOOST_TEST(ai.FindBestPosition(around, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); + BOOST_TEST(!ai.FindBestPosition(around, AIResource::Ironore, BuildingQuality::Mine, 5).isValid()); +} + +BOOST_FIXTURE_TEST_CASE(GraniteLegacyWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) +{ + makeWorldMineable(world); + ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); + + AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); + makeMineNodesUsableForSearch(ai, world, 0); + + const MapPoint around = world.GetPlayer(0).GetHQPos(); + BOOST_TEST(ai.FindBestPosition(around, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); + BOOST_TEST(!ai.FindBestPosition(around, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); +} + +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteDoesNotImplyWorkEverywhereForAI, EmptyWorldFixture1P) +{ + makeWorldMineable(world); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); + + AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); + makeMineNodesUsableForSearch(ai, world, 0); + + const MapPoint around = world.GetPlayer(0).GetHQPos(); + BOOST_TEST(!ai.FindBestPosition(around, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); +} + BOOST_FIXTURE_TEST_CASE(KeepBQUpdated, BiggerWorldWithGCExecution) { addStartResources(); From d1df4856370db4f40addcc3121d38fa23a705cda Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 16 May 2026 00:23:34 +0200 Subject: [PATCH 10/23] Adjust mine productivity display for S4-like behavior --- libs/s25main/BuildingRegister.cpp | 25 +++++++++++++++ libs/s25main/BuildingRegister.h | 4 +++ libs/s25main/buildings/nobUsual.cpp | 22 +++++++++++++ libs/s25main/buildings/nobUsual.h | 1 + libs/s25main/figures/nofMiner.cpp | 11 ++----- .../gameTypes/MineResourceBehavior.cpp | 23 ++++++++++++++ libs/s25main/gameTypes/MineResourceBehavior.h | 5 +++ libs/s25main/ingameWindows/iwBuilding.cpp | 5 +-- libs/s25main/ingameWindows/iwBuilding.h | 1 + .../iwBuildingProductivities.cpp | 2 +- tests/s25Main/integration/testGamePlayer.cpp | 31 +++++++++++++++++++ 11 files changed, 118 insertions(+), 12 deletions(-) diff --git a/libs/s25main/BuildingRegister.cpp b/libs/s25main/BuildingRegister.cpp index 13a5e8094b..a2e52076a4 100644 --- a/libs/s25main/BuildingRegister.cpp +++ b/libs/s25main/BuildingRegister.cpp @@ -155,6 +155,15 @@ helpers::EnumArray BuildingRegister::CalcProductivities( return productivities; } +helpers::EnumArray BuildingRegister::CalcDisplayProductivities() const +{ + helpers::EnumArray productivities; + + for(const auto bld : helpers::enumRange()) + productivities[bld] = static_cast(CalcAverageDisplayProductivity(bld)); + return productivities; +} + unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const { if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) @@ -171,6 +180,22 @@ unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const return productivity; } +unsigned BuildingRegister::CalcAverageDisplayProductivity(BuildingType bldType) const +{ + if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) + return 0; + unsigned productivity = 0; + const auto& buildings = GetBuildings(bldType); + const unsigned numBlds = buildings.size(); + if(numBlds > 0) + { + for(const nobUsual* bld : buildings) + productivity += bld->GetDisplayProductivity(); + productivity /= numBlds; + } + return productivity; +} + unsigned short BuildingRegister::CalcAverageProductivity() const { unsigned totalProductivity = 0; diff --git a/libs/s25main/BuildingRegister.h b/libs/s25main/BuildingRegister.h index bcc5fb488c..a70f0e1ffd 100644 --- a/libs/s25main/BuildingRegister.h +++ b/libs/s25main/BuildingRegister.h @@ -41,8 +41,12 @@ class BuildingRegister BuildingCount GetBuildingNums() const; /// Calculate and fill the average productivities for all buildings. helpers::EnumArray CalcProductivities() const; + /// Calculate and fill the average productivities shown in UI. + helpers::EnumArray CalcDisplayProductivities() const; /// Calculate the average productivity for a building type unsigned CalcAverageProductivity(BuildingType bldType) const; + /// Calculate the average productivity shown in UI for a building type + unsigned CalcAverageDisplayProductivity(BuildingType bldType) const; /// Calculate the average productivity for all buildings unsigned short CalcAverageProductivity() const; diff --git a/libs/s25main/buildings/nobUsual.cpp b/libs/s25main/buildings/nobUsual.cpp index 9ed430b514..c3b1a0b68e 100644 --- a/libs/s25main/buildings/nobUsual.cpp +++ b/libs/s25main/buildings/nobUsual.cpp @@ -19,8 +19,11 @@ #include "ogl/glArchivItem_Bitmap_Player.h" #include "postSystem/PostMsgWithBuilding.h" #include "world/GameWorld.h" +#include "gameTypes/MineResourceBehavior.h" +#include "gameTypes/Resource.h" #include "gameData/BuildingConsts.h" #include "gameData/BuildingProperties.h" +#include "gameData/GameConsts.h" #include /// Number of GFs after which the productivity is recalculated, i.e. productivity is averaged over intervals of this @@ -519,6 +522,25 @@ bool nobUsual::HasWorker() const return worker && worker->GetState() != nofBuildingWorker::State::FigureWork; } +unsigned short nobUsual::GetDisplayProductivity() const +{ + if(!BuildingProperties::IsMine(bldType_) + || GetEffectiveMineResourceBehavior(world->GetGGS(), bldType_) != MineResourceBehavior::S4LikeExhaustion) + return productivity; + + const ResourceType resourceType = GetMineResourceType(bldType_); + const std::vector resourcePts = world->GetMatchingPointsInRadius<1>( + pos, MINER_RADIUS, + [this, resourceType](const MapPoint pt) { return world->GetNode(pt).resources.has(resourceType); }, true); + + unsigned resourceAmount = 0; + for(const MapPoint pt : resourcePts) + resourceAmount += world->GetNode(pt).resources.getAmount(); + + return static_cast( + (static_cast(productivity) * GetS4LikeMineProductionChance(resourceAmount)) / 100u); +} + void nobUsual::OnOutOfResources() { // Post verschicken, keine Rohstoffe mehr da diff --git a/libs/s25main/buildings/nobUsual.h b/libs/s25main/buildings/nobUsual.h index 5b7cf0da1a..436c61c8a7 100644 --- a/libs/s25main/buildings/nobUsual.h +++ b/libs/s25main/buildings/nobUsual.h @@ -102,6 +102,7 @@ class nobUsual : public noBuilding /// Gibt Pointer auf Produktivität zurück const unsigned short* GetProductivityPointer() const { return &productivity; } unsigned short GetProductivity() const { return productivity; } + unsigned short GetDisplayProductivity() const; const nofBuildingWorker* GetWorker() const { return worker; } /// Stoppt/Erlaubt Produktion (visuell) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index b7e3b47e4e..f971860d1a 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -23,7 +23,6 @@ namespace { constexpr unsigned MAX_PRODUCTION_PERCENT = 100; constexpr unsigned GRANITE_FALLBACK_25_PERCENT = 25; constexpr unsigned GRANITE_FALLBACK_50_PERCENT = 50; -constexpr unsigned S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE = 5; constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; constexpr uint8_t WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT = 8; constexpr unsigned WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS = 8; @@ -46,7 +45,7 @@ unsigned GetS4LikeProductionChance(const GameWorld& world, const std::vectorGetBuildingType()) - { - case BuildingType::GoldMine: return ResourceType::Gold; - case BuildingType::IronMine: return ResourceType::Iron; - case BuildingType::CoalMine: return ResourceType::Coal; - default: return ResourceType::Granite; - } + return GetMineResourceType(workplace->GetBuildingType()); } diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 646646c057..7b41468c14 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -6,6 +6,13 @@ #include "GlobalGameSettings.h" #include "addons/const_addons.h" #include "gameTypes/BuildingType.h" +#include "gameTypes/Resource.h" +#include + +namespace { +constexpr unsigned MAX_PRODUCTION_PERCENT = 100; +constexpr unsigned S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE = 5; +} // namespace AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) { @@ -18,6 +25,22 @@ AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) } } +ResourceType GetMineResourceType(const BuildingType buildingType) +{ + switch(buildingType) + { + case BuildingType::GoldMine: return ResourceType::Gold; + case BuildingType::IronMine: return ResourceType::Iron; + case BuildingType::CoalMine: return ResourceType::Coal; + default: return ResourceType::Granite; + } +} + +unsigned GetS4LikeMineProductionChance(const unsigned resourceAmount) +{ + return std::min(MAX_PRODUCTION_PERCENT, resourceAmount * S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE); +} + MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) { diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index 60c98ba0b7..0e6914e00d 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -4,9 +4,12 @@ #pragma once +#include + class GlobalGameSettings; enum class AddonId; enum class BuildingType : unsigned char; +enum class ResourceType : uint8_t; enum class MineResourceBehavior : unsigned { @@ -17,6 +20,8 @@ enum class MineResourceBehavior : unsigned }; AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); +ResourceType GetMineResourceType(BuildingType buildingType); +unsigned GetS4LikeMineProductionChance(unsigned resourceAmount); MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); MineResourceBehavior GetEffectiveMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); bool IsMineResourceDepletable(const GlobalGameSettings& settings, BuildingType buildingType); diff --git a/libs/s25main/ingameWindows/iwBuilding.cpp b/libs/s25main/ingameWindows/iwBuilding.cpp index 55f48f66bf..aac62f5dc4 100644 --- a/libs/s25main/ingameWindows/iwBuilding.cpp +++ b/libs/s25main/ingameWindows/iwBuilding.cpp @@ -32,7 +32,7 @@ const unsigned IODAT_SHIP_ID = 218; iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsual* const building, Extent extent) : IngameWindow(CGI_BUILDING + MapBase::CreateGUIID(building->GetPos()), IngameWindow::posAtMouse, extent, _(BUILDING_NAMES[building->GetBuildingType()]), LOADER.GetImageN("resource", 41)), - gwv(gwv), gcFactory(gcFactory), building(building) + gwv(gwv), gcFactory(gcFactory), building(building), displayProductivity(building->GetDisplayProductivity()) { // Arbeitersymbol AddImage(0, DrawPoint(28, 39), LOADER.GetMapTexture(2298)); @@ -84,7 +84,7 @@ iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsu // Produktivitätsanzeige (bei Katapulten und Spähtürmen ausblenden) Window* productivity = AddPercent(9, DrawPoint(59, 31), Extent(106, 16), TextureColor::Grey, 0xFFFFFF00, SmallFont, - building->GetProductivityPointer()); + &displayProductivity); if(building->GetBuildingType() == BuildingType::Catapult || building->GetBuildingType() == BuildingType::LookoutTower) productivity->SetVisible(false); @@ -99,6 +99,7 @@ iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsu void iwBuilding::Msg_PaintBefore() { IngameWindow::Msg_PaintBefore(); + displayProductivity = building->GetDisplayProductivity(); // Haus unbesetzt ggf ausblenden GetCtrl(10)->SetVisible(!building->HasWorker()); diff --git a/libs/s25main/ingameWindows/iwBuilding.h b/libs/s25main/ingameWindows/iwBuilding.h index 0feb36adf6..96daa9bd2c 100644 --- a/libs/s25main/ingameWindows/iwBuilding.h +++ b/libs/s25main/ingameWindows/iwBuilding.h @@ -16,6 +16,7 @@ class iwBuilding : public IngameWindow GameWorldView& gwv; GameCommandFactory& gcFactory; nobUsual* const building; /// Das zugehörige Gebäudeobjekt + unsigned short displayProductivity; public: iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsual* building, Extent extent = Extent(226, 194)); diff --git a/libs/s25main/ingameWindows/iwBuildingProductivities.cpp b/libs/s25main/ingameWindows/iwBuildingProductivities.cpp index af44722c60..4f5e04d605 100644 --- a/libs/s25main/ingameWindows/iwBuildingProductivities.cpp +++ b/libs/s25main/ingameWindows/iwBuildingProductivities.cpp @@ -100,7 +100,7 @@ iwBuildingProductivities::iwBuildingProductivities(const GamePlayer& player) void iwBuildingProductivities::UpdatePercents() { - percents = player.GetBuildingRegister().CalcProductivities(); + percents = player.GetBuildingRegister().CalcDisplayProductivities(); } void iwBuildingProductivities::Msg_PaintAfter() diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index b594e4e2bc..cc9dc96f65 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -12,6 +12,8 @@ #include "ingameWindows/iwBuildingProductivities.h" #include "worldFixtures/CreateEmptyWorld.h" #include "worldFixtures/WorldFixture.h" +#include "gameTypes/MineResourceBehavior.h" +#include "gameTypes/Resource.h" #include "gameData/BuildingProperties.h" #include "rttr/test/random.hpp" #include "s25util/warningSuppression.h" @@ -121,6 +123,35 @@ BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) BOOST_TEST(buildingRegister.CalcAverageProductivity() == avgProd); } +BOOST_FIXTURE_TEST_CASE(MineDisplayProductivityAccountsForS4LikeResourceChance, WorldFixtureEmpty1P) +{ + MapPoint minePos(0, 0); + while(world.GetNode(minePos).bq != BuildingQuality::Castle) + BOOST_TEST_REQUIRE((++minePos.x) < world.GetSize().x); + + auto* coalMine = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::CoalMine, minePos, 0, Nation::Romans)); + setProductivity(coalMine, 100); + + world.SetResource(minePos, Resource(ResourceType::Coal, 1)); + BOOST_TEST(coalMine->GetProductivity() == 100u); + BOOST_TEST(coalMine->GetDisplayProductivity() == 100u); + + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + BOOST_TEST(coalMine->GetProductivity() == 100u); + BOOST_TEST(coalMine->GetDisplayProductivity() == 5u); + + setProductivity(coalMine, 80); + world.SetResource(minePos, Resource(ResourceType::Coal, 10)); + BOOST_TEST(coalMine->GetDisplayProductivity() == 40u); + BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcDisplayProductivities()[BuildingType::CoalMine] == 40u); + + setProductivity(coalMine, 100); + world.SetResource(minePos, Resource(ResourceType::Coal, 15)); + BOOST_TEST(coalMine->GetDisplayProductivity() == 75u); +} + BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) { GamePlayer& p1 = world.GetPlayer(0); From bf241069eb00af76f7da9ffd36f8ba12115d525b Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 26 May 2026 16:23:57 +0200 Subject: [PATCH 11/23] Rework configurable mine resource behavior --- libs/s25main/BuildingRegister.cpp | 50 +++++++-------- libs/s25main/BuildingRegister.h | 2 - libs/s25main/GlobalGameSettings.cpp | 64 +++++++++++++++++-- .../addons/AddonGraniteMinesWorkEverywhere.h | 21 ------ .../addons/AddonInexhaustibleGraniteMines.h | 13 ++-- libs/s25main/addons/AddonInexhaustibleMines.h | 6 +- .../addons/AddonMineResourceBehavior.h | 11 +--- libs/s25main/addons/Addons.h | 1 - libs/s25main/addons/const_addons.h | 8 +-- libs/s25main/ai/AIInterface.cpp | 26 +++----- libs/s25main/ai/AIResource.h | 15 +++++ libs/s25main/ai/aijh/AIPlayerJH.cpp | 4 +- libs/s25main/ai/aijh/BuildingPlanner.cpp | 15 ++++- libs/s25main/buildings/nobUsual.cpp | 2 +- libs/s25main/figures/nofMiner.cpp | 64 +++++-------------- libs/s25main/figures/nofMiner.h | 1 - libs/s25main/figures/nofWorkman.cpp | 5 +- libs/s25main/figures/nofWorkman.h | 2 +- .../gameTypes/MineResourceBehavior.cpp | 57 ++++++----------- libs/s25main/gameTypes/MineResourceBehavior.h | 13 ++-- tests/s25Main/integration/testAI.cpp | 8 ++- tests/s25Main/integration/testProduction.cpp | 35 ++++------ .../s25Main/integration/testSerialization.cpp | 53 +++++++++++++++ 23 files changed, 258 insertions(+), 218 deletions(-) delete mode 100644 libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h diff --git a/libs/s25main/BuildingRegister.cpp b/libs/s25main/BuildingRegister.cpp index a2e52076a4..c26c8a6162 100644 --- a/libs/s25main/BuildingRegister.cpp +++ b/libs/s25main/BuildingRegister.cpp @@ -17,6 +17,21 @@ #include "gameData/BuildingConsts.h" #include "gameData/BuildingProperties.h" +namespace { +unsigned CalcAverageProductivity(const std::list& buildings, + unsigned short (nobUsual::*getProductivity)() const) +{ + const unsigned numBlds = buildings.size(); + if(numBlds == 0) + return 0; + + unsigned productivity = 0; + for(const nobUsual* bld : buildings) + productivity += (bld->*getProductivity)(); + return productivity / numBlds; +} +} // namespace + void BuildingRegister::Serialize(SerializedGameData& sgd) const { sgd.PushObjectContainer(warehouses); @@ -160,40 +175,21 @@ helpers::EnumArray BuildingRegister::CalcDisplayProducti helpers::EnumArray productivities; for(const auto bld : helpers::enumRange()) - productivities[bld] = static_cast(CalcAverageDisplayProductivity(bld)); - return productivities; -} - -unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const -{ - if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) - return 0; - unsigned productivity = 0; - const auto& buildings = GetBuildings(bldType); - const unsigned numBlds = buildings.size(); - if(numBlds > 0) { - for(const nobUsual* bld : buildings) - productivity += bld->GetProductivity(); - productivity /= numBlds; + if(holds_alternative(BLD_WORK_DESC[bld].producedWare)) + productivities[bld] = 0; + else + productivities[bld] = + static_cast(::CalcAverageProductivity(GetBuildings(bld), &nobUsual::GetDisplayProductivity)); } - return productivity; + return productivities; } -unsigned BuildingRegister::CalcAverageDisplayProductivity(BuildingType bldType) const +unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const { if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) return 0; - unsigned productivity = 0; - const auto& buildings = GetBuildings(bldType); - const unsigned numBlds = buildings.size(); - if(numBlds > 0) - { - for(const nobUsual* bld : buildings) - productivity += bld->GetDisplayProductivity(); - productivity /= numBlds; - } - return productivity; + return ::CalcAverageProductivity(GetBuildings(bldType), &nobUsual::GetProductivity); } unsigned short BuildingRegister::CalcAverageProductivity() const diff --git a/libs/s25main/BuildingRegister.h b/libs/s25main/BuildingRegister.h index a70f0e1ffd..003133e130 100644 --- a/libs/s25main/BuildingRegister.h +++ b/libs/s25main/BuildingRegister.h @@ -45,8 +45,6 @@ class BuildingRegister helpers::EnumArray CalcDisplayProductivities() const; /// Calculate the average productivity for a building type unsigned CalcAverageProductivity(BuildingType bldType) const; - /// Calculate the average productivity shown in UI for a building type - unsigned CalcAverageDisplayProductivity(BuildingType bldType) const; /// Calculate the average productivity for all buildings unsigned short CalcAverageProductivity() const; diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 2094af1037..ac8a70ff7f 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -8,15 +8,44 @@ #include "addons/Addons.h" #include "helpers/containerUtils.h" #include "helpers/serializeEnums.h" +#include "gameTypes/BuildingType.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameData/MilitaryConsts.h" #include "s25util/Log.h" #include "s25util/Serializer.h" #include #include #include +#include #include #include +namespace { +constexpr std::array MINE_BUILDING_TYPES = {BuildingType::GraniteMine, BuildingType::CoalMine, + BuildingType::IronMine, BuildingType::GoldMine}; + +helpers::OptionalEnum GetMineBuildingTypeForAddonId(const AddonId id) +{ + for(const BuildingType mineType : MINE_BUILDING_TYPES) + { + if(GetMineResourceBehaviorAddonId(mineType) == id) + return mineType; + } + return boost::none; +} + +void MigrateLegacyInexhaustibleMines(GlobalGameSettings& settings, + const helpers::EnumArray& hasMineBehaviorSetting) +{ + for(const BuildingType mineType : MINE_BUILDING_TYPES) + { + if(!hasMineBehaviorSetting[mineType]) + settings.setSelection(GetMineResourceBehaviorAddonId(mineType), + static_cast(MineResourceBehavior::Inexhaustible)); + } +} +} // namespace + GlobalGameSettings::GlobalGameSettings() : speed(GameSpeed::Normal), objective(GameObjective::None), startWares(StartWares::Normal), lockedTeams(false), exploration(Exploration::FogOfWar), teamView(true), randomStartPosition(false) @@ -79,13 +108,10 @@ void GlobalGameSettings::registerAllAddons() AddonHalfCostMilEquip, AddonInexhaustibleFish, AddonInexhaustibleGraniteMines, - AddonGraniteMinesWorkEverywhere, AddonCoalMineResourceBehavior, AddonIronMineResourceBehavior, AddonGoldMineResourceBehavior, - AddonGraniteMineResourceBehavior, AddonMineNoOutputFallback, - AddonInexhaustibleMines, AddonLimitCatapults, AddonManualRoadEnlargement, AddonMaxRank, @@ -193,8 +219,25 @@ void GlobalGameSettings::LoadSettings() { resetAddons(); + bool migrateLegacyInexhaustibleMines = false; + helpers::EnumArray hasMineBehaviorSetting{}; for(const auto& it : SETTINGS.addons.configuration) - setSelection(static_cast(it.first), it.second); + { + const auto id = static_cast(it.first); + const unsigned status = it.second; + if(id == AddonId::INEXHAUSTIBLE_MINES) + { + migrateLegacyInexhaustibleMines = status != 0; + continue; + } + + if(const auto mineType = GetMineBuildingTypeForAddonId(id)) + hasMineBehaviorSetting[*mineType] = true; + + setSelection(id, status); + } + if(migrateLegacyInexhaustibleMines) + MigrateLegacyInexhaustibleMines(*this, hasMineBehaviorSetting); } /** @@ -249,12 +292,25 @@ void GlobalGameSettings::Deserialize(Serializer& ser) resetAddons(); + bool migrateLegacyInexhaustibleMines = false; + helpers::EnumArray hasMineBehaviorSetting{}; for(unsigned i = 0; i < count; ++i) { auto addon = static_cast(ser.PopUnsignedInt()); unsigned status = ser.PopUnsignedInt(); + if(addon == AddonId::INEXHAUSTIBLE_MINES) + { + migrateLegacyInexhaustibleMines = status != 0; + continue; + } + + if(const auto mineType = GetMineBuildingTypeForAddonId(addon)) + hasMineBehaviorSetting[*mineType] = true; + setSelection(addon, status); } + if(migrateLegacyInexhaustibleMines) + MigrateLegacyInexhaustibleMines(*this, hasMineBehaviorSetting); } void GlobalGameSettings::setSelection(AddonId id, unsigned selection) diff --git a/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h b/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h deleted file mode 100644 index ab5bd5c3b8..0000000000 --- a/libs/s25main/addons/AddonGraniteMinesWorkEverywhere.h +++ /dev/null @@ -1,21 +0,0 @@ -// 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 "const_addons.h" -#include "mygettext/mygettext.h" - -/** - * Addon for creating finite granite resources below granite mines without explicit stone resources. - */ -class AddonGraniteMinesWorkEverywhere : public AddonBool -{ -public: - AddonGraniteMinesWorkEverywhere() - : AddonBool(AddonId::GRANITEMINES_WORK_EVERYWHERE, AddonGroup::Economy, _("Granite Mines Work Everywhere"), - _("Granite mines can create a finite stone resource on otherwise empty mountain spots.")) - {} -}; diff --git a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h index 592895651e..80c95ca8c3 100644 --- a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h +++ b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h @@ -4,17 +4,20 @@ #pragma once -#include "AddonBool.h" +#include "AddonMineResourceBehavior.h" #include "mygettext/mygettext.h" /** - * Addon for allowing granite mines to have unlimited resources. + * Granite mine resource behavior list. + * + * Reuses the old boolean INEXHAUSTIBLE_GRANITEMINES id: saved value 0 still means default behavior and saved value 1 + * now selects the inexhaustible behavior. */ -class AddonInexhaustibleGraniteMines : public AddonBool +class AddonInexhaustibleGraniteMines : public AddonMineResourceBehaviorBase { public: AddonInexhaustibleGraniteMines() - : AddonBool(AddonId::INEXHAUSTIBLE_GRANITEMINES, AddonGroup::Economy, _("Inexhaustible Granite Mines"), - _("Granite mines will never deplete stone resources.")) + : AddonMineResourceBehaviorBase(AddonId::INEXHAUSTIBLE_GRANITEMINES, _("Granite Mine Resource Behavior"), + _("Configures how granite mines consume and exhaust stone deposits.")) {} }; diff --git a/libs/s25main/addons/AddonInexhaustibleMines.h b/libs/s25main/addons/AddonInexhaustibleMines.h index 5f4f342eb1..edb28f5b83 100644 --- a/libs/s25main/addons/AddonInexhaustibleMines.h +++ b/libs/s25main/addons/AddonInexhaustibleMines.h @@ -8,7 +8,11 @@ #include "mygettext/mygettext.h" /** - * Addon for allowing to have unlimited resources. + * Deprecated global mine setting. + * + * Not registered anymore. The ID is still decoded when loading old + * settings/savegames and migrated to the per-mine + * resource behavior settings. */ class AddonInexhaustibleMines : public AddonBool { diff --git a/libs/s25main/addons/AddonMineResourceBehavior.h b/libs/s25main/addons/AddonMineResourceBehavior.h index e9a443f168..5ae0e9c960 100644 --- a/libs/s25main/addons/AddonMineResourceBehavior.h +++ b/libs/s25main/addons/AddonMineResourceBehavior.h @@ -15,7 +15,7 @@ class AddonMineResourceBehaviorBase : public AddonList protected: AddonMineResourceBehaviorBase(AddonId id, const std::string& name, const std::string& description) : AddonList(id, AddonGroup::Economy, name, description, - {_("Default"), _("S4-like exhaustion"), _("Inexhaustible"), _("Work everywhere")}, + {_("Default"), _("Inexhaustible"), _("S4-like exhaustion"), _("Work everywhere")}, static_cast(MineResourceBehavior::Default)) {} }; @@ -46,12 +46,3 @@ class AddonGoldMineResourceBehavior : public AddonMineResourceBehaviorBase _("Configures how gold mines consume and exhaust gold deposits.")) {} }; - -class AddonGraniteMineResourceBehavior : public AddonMineResourceBehaviorBase -{ -public: - AddonGraniteMineResourceBehavior() - : AddonMineResourceBehaviorBase(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, _("Granite Mine Resource Behavior"), - _("Configures how granite mines consume and exhaust stone deposits.")) - {} -}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 43d1580afd..8bbc7c68c4 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -33,7 +33,6 @@ #include "addons/AddonToolOrdering.h" -#include "addons/AddonGraniteMinesWorkEverywhere.h" #include "addons/AddonInexhaustibleFish.h" #include "addons/AddonInexhaustibleGraniteMines.h" #include "addons/AddonMaxRank.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index fd32afa694..89db9e5622 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -38,6 +38,7 @@ // // Add the #include for your AddonXXX.h in Addons.h! // +// TODO: INEXHAUSTIBLE_MINES is kept only for loading old settings/savegames until the gamedata version can be raised. ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x00000001, REFUND_MATERIALS = 0x00000002, EXHAUSTIBLE_WATER = 0x00000003, REFUND_ON_EMERGENCY = 0x00000004, MANUAL_ROAD_ENLARGEMENT = 0x00000005, CATAPULT_GRAPHICS = 0x00000006, METALWORKSBEHAVIORONZERO = 0x00000007, @@ -57,10 +58,9 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, - INEXHAUSTIBLE_GRANITEMINES = 0x00800000, GRANITEMINES_WORK_EVERYWHERE = 0x00800001, - COALMINE_RESOURCE_BEHAVIOR = 0x00800002, IRONMINE_RESOURCE_BEHAVIOR = 0x00800003, - GOLDMINE_RESOURCE_BEHAVIOR = 0x00800004, GRANITEMINE_RESOURCE_BEHAVIOR = 0x00800005, - MINE_NO_OUTPUT_FALLBACK = 0x00800006, + INEXHAUSTIBLE_GRANITEMINES = 0x00800000, COALMINE_RESOURCE_BEHAVIOR = 0x00800001, + IRONMINE_RESOURCE_BEHAVIOR = 0x00800002, GOLDMINE_RESOURCE_BEHAVIOR = 0x00800003, + MINE_NO_OUTPUT_FALLBACK = 0x00800004, MAX_RANK = 0x00900000, SEA_ATTACK = 0x00900001, INEXHAUSTIBLE_FISH = 0x00900002, MORE_ANIMALS = 0x00900003, BURN_DURATION = 0x00900004, NO_ALLIED_PUSH = 0x00900005, diff --git a/libs/s25main/ai/AIInterface.cpp b/libs/s25main/ai/AIInterface.cpp index 1f0104a852..fe714702e0 100644 --- a/libs/s25main/ai/AIInterface.cpp +++ b/libs/s25main/ai/AIInterface.cpp @@ -47,18 +47,6 @@ bool IsPointOK_RoadPathEvenStep(const GameWorldBase& gwb, const MapPoint pt, con return prp->boat_road || gwb.GetBQ(pt, gwb.GetNode(pt).owner - 1) != BuildingQuality::Nothing; } -helpers::OptionalEnum GetMineBuildingType(const AIResource res) -{ - switch(res) - { - case AIResource::Gold: return BuildingType::GoldMine; - case AIResource::Ironore: return BuildingType::IronMine; - case AIResource::Coal: return BuildingType::CoalMine; - case AIResource::Granite: return BuildingType::GraniteMine; - default: return boost::none; - } -} - int GetS4LikeMineResourceRating(const Resource resource, const unsigned defaultRating) { if(resource.getAmount() == 0u) @@ -183,15 +171,15 @@ int AIInterface::GetResourceRating(const MapPoint pt, AIResource res) const const Resource subres = gwb.GetNode(pt).resources; if(convertToNodeResource(GetSubsurfaceResource(pt)) == res) { - const auto mineBuildingType = GetMineBuildingType(res); + const auto mineBuildingType = GetMineBuildingType(subres.getType()); if(mineBuildingType - && GetEffectiveMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) + && GetMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) == MineResourceBehavior::S4LikeExhaustion) return GetS4LikeMineResourceRating(subres, RES_RADIUS[res]); return RES_RADIUS[res]; } - if(IsMineResourceWorkEverywhere(res) && subres.getType() == ResourceType::Nothing + if(IsMineResourceWorkEverywhere(res) && gwb.IsOfTerrain(pt, [](const TerrainDesc& desc) { return desc.Is(ETerrain::Mineable); })) return RES_RADIUS[res]; break; @@ -206,9 +194,13 @@ int AIInterface::GetResourceRating(const MapPoint pt, AIResource res) const bool AIInterface::IsMineResourceWorkEverywhere(const AIResource res) const { - const auto mineBuildingType = GetMineBuildingType(res); + const auto resourceType = convertToResourceType(res); + if(!resourceType) + return false; + + const auto mineBuildingType = GetMineBuildingType(*resourceType); return mineBuildingType - && GetEffectiveMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) == MineResourceBehavior::WorkEverywhere; + && GetMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) == MineResourceBehavior::WorkEverywhere; } int AIInterface::CalcResourceValue(const MapPoint pt, AIResource res, helpers::OptionalEnum direction, diff --git a/libs/s25main/ai/AIResource.h b/libs/s25main/ai/AIResource.h index 9d2270e74b..00f01bfca0 100644 --- a/libs/s25main/ai/AIResource.h +++ b/libs/s25main/ai/AIResource.h @@ -5,6 +5,8 @@ #pragma once #include "helpers/EnumArray.h" +#include "helpers/OptionalEnum.h" +#include "gameTypes/Resource.h" #include "s25util/warningSuppression.h" // Note: This enums are constructed for performance and easy conversion. @@ -86,6 +88,19 @@ constexpr bool operator==(AINodeResource lhs, AIResource rhs) return lhs == convertToNodeResource(rhs); } +inline helpers::OptionalEnum convertToResourceType(AIResource res) +{ + switch(res) + { + case AIResource::Gold: return ResourceType::Gold; + case AIResource::Ironore: return ResourceType::Iron; + case AIResource::Coal: return ResourceType::Coal; + case AIResource::Granite: return ResourceType::Granite; + case AIResource::Fish: return ResourceType::Fish; + default: return boost::none; + } +} + constexpr helpers::EnumArray SUPPRESS_UNUSED RES_RADIUS = { 2, // Gold 2, // Ironore diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index 7c7eb0d0c7..78eacd5263 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -153,8 +153,8 @@ static bool isUnlimitedResource(const AIResource res, const GlobalGameSettings& template static auto createResourceMaps(const AIInterface& aii, const AIMap& aiMap, std::index_sequence) { - return helpers::EnumArray{ - AIResourceMap(AIResource(I), isUnlimitedResource(AIResource(I), aii.gwb.GetGGS()), aii, aiMap)...}; + return helpers::EnumArray{AIResourceMap( + static_cast(I), isUnlimitedResource(static_cast(I), aii.gwb.GetGGS()), aii, aiMap)...}; } static auto createResourceMaps(const AIInterface& aii, const AIMap& aiMap) { diff --git a/libs/s25main/ai/aijh/BuildingPlanner.cpp b/libs/s25main/ai/aijh/BuildingPlanner.cpp index f626d89477..736ccfba9b 100644 --- a/libs/s25main/ai/aijh/BuildingPlanner.cpp +++ b/libs/s25main/ai/aijh/BuildingPlanner.cpp @@ -10,11 +10,21 @@ #include "buildings/nobMilitary.h" #include "gameTypes/BuildingType.h" #include "gameTypes/GoodTypes.h" +#include "gameTypes/MineResourceBehavior.h" #include "gameData/BuildingProperties.h" #include #include namespace AIJH { +namespace { + bool HasAnyInexhaustibleOreMine(const GlobalGameSettings& ggs) + { + return !IsMineResourceDepletable(ggs, BuildingType::CoalMine) + || !IsMineResourceDepletable(ggs, BuildingType::IronMine) + || !IsMineResourceDepletable(ggs, BuildingType::GoldMine); + } +} // namespace + BuildingPlanner::BuildingPlanner(const AIPlayerJH& aijh) : buildingsWanted(), expansionRequired(false) { RefreshBuildingNums(aijh); @@ -224,7 +234,7 @@ void BuildingPlanner::UpdateBuildingsWanted(const AIPlayerJH& aijh) // brewery count = 1+(armory/5) if there is at least 1 armory or armory /6 for exhaustible mines if(GetNumBuildings(BuildingType::Armory) > 0 && GetNumBuildings(BuildingType::Farm) > 0) { - if(aijh.ggs.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) + if(HasAnyInexhaustibleOreMine(aijh.ggs)) buildingsWanted[BuildingType::Brewery] = 1 + GetNumBuildings(BuildingType::Armory) / 5; else buildingsWanted[BuildingType::Brewery] = 1 + GetNumBuildings(BuildingType::Armory) / 6; @@ -292,8 +302,7 @@ void BuildingPlanner::UpdateBuildingsWanted(const AIPlayerJH& aijh) (GetNumBuildings(BuildingType::Farm) + GetNumBuildings(BuildingType::Fishery)) / 2 + 2; if(GetNumBuildings(BuildingType::Farm) > 7) // quite the empire just scale mines with farms { - if(aijh.ggs.isEnabled( - AddonId::INEXHAUSTIBLE_MINES)) // inexhaustible mines? -> more farms required for each mine + if(HasAnyInexhaustibleOreMine(aijh.ggs)) // inexhaustible mines? -> more farms required for each mine buildingsWanted[BuildingType::IronMine] = std::min(GetNumBuildings(BuildingType::Ironsmelter) + 1, GetNumBuildings(BuildingType::Farm) * 2 / 5); else diff --git a/libs/s25main/buildings/nobUsual.cpp b/libs/s25main/buildings/nobUsual.cpp index c3b1a0b68e..39ff3acddd 100644 --- a/libs/s25main/buildings/nobUsual.cpp +++ b/libs/s25main/buildings/nobUsual.cpp @@ -525,7 +525,7 @@ bool nobUsual::HasWorker() const unsigned short nobUsual::GetDisplayProductivity() const { if(!BuildingProperties::IsMine(bldType_) - || GetEffectiveMineResourceBehavior(world->GetGGS(), bldType_) != MineResourceBehavior::S4LikeExhaustion) + || GetMineResourceBehavior(world->GetGGS(), bldType_) != MineResourceBehavior::S4LikeExhaustion) return productivity; const ResourceType resourceType = GetMineResourceType(bldType_); diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index f971860d1a..2cfe2af638 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -24,8 +24,6 @@ constexpr unsigned MAX_PRODUCTION_PERCENT = 100; constexpr unsigned GRANITE_FALLBACK_25_PERCENT = 25; constexpr unsigned GRANITE_FALLBACK_50_PERCENT = 50; constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; -constexpr uint8_t WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT = 8; -constexpr unsigned WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS = 8; MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& settings) { @@ -39,7 +37,8 @@ MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& set } } -unsigned GetS4LikeProductionChance(const GameWorld& world, const std::vector& resourcePts) +unsigned GetS4LikeProductionChanceForRemainingResources(const GameWorld& world, + const std::vector& resourcePts) { unsigned resourceAmount = 0; for(const MapPoint pt : resourcePts) @@ -97,25 +96,6 @@ std::vector GetPointsWithResource(const GameWorld& world, const MapPoi pos, MINER_RADIUS, [&world, type](const MapPoint pt) { return world.GetNode(pt).resources.has(type); }, true); } -bool CanCreateWorkEverywhereResource(const GameWorld& world, const MapPoint pos, const MineResourceBehavior behavior) -{ - return behavior == MineResourceBehavior::WorkEverywhere - && world.GetNode(pos).resources.getType() == ResourceType::Nothing; -} - -MapPoint CreateWorkEverywhereResource(GameWorld& world, const MapPoint pos, const ResourceType type, - const MineResourceBehavior behavior, const unsigned objId) -{ - if(!CanCreateWorkEverywhereResource(world, pos, behavior)) - return MapPoint::Invalid(); - - const auto amount = - static_cast(WORK_EVERYWHERE_RESOURCE_MIN_AMOUNT - + RANDOM.Rand(RANDOM_CONTEXT2(objId), WORK_EVERYWHERE_RESOURCE_AMOUNT_VARIANTS)); - world.SetResource(pos, Resource(type, amount)); - return pos; -} - void ReduceS4LikeResource(GameWorld& world, const std::vector& resourcePts) { for(const MapPoint pt : resourcePts) @@ -178,15 +158,14 @@ unsigned short nofMiner::GetCarryID() const helpers::OptionalEnum nofMiner::ProduceWare() { const GlobalGameSettings& settings = world->GetGGS(); - const MineResourceBehavior effectiveBehavior = - GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); + const MineResourceBehavior behavior = GetMineResourceBehavior(settings, workplace->GetBuildingType()); - if(effectiveBehavior == MineResourceBehavior::S4LikeExhaustion) + if(behavior == MineResourceBehavior::S4LikeExhaustion) { const std::vector resourcePts = GetPointsWithResource(*world, pos, GetRequiredResType()); const auto productionRoll = static_cast(RANDOM_RAND(MAX_PRODUCTION_PERCENT)); const bool produceNothingThisCycle = - resourcePts.empty() || productionRoll >= GetS4LikeProductionChance(*world, resourcePts); + resourcePts.empty() || productionRoll >= GetS4LikeProductionChanceForRemainingResources(*world, resourcePts); if(produceNothingThisCycle) return GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); @@ -203,22 +182,16 @@ helpers::OptionalEnum nofMiner::ProduceWare() } } -MapPoint nofMiner::FindPointWithResourceQuiet(ResourceType type) const -{ - const auto pts = GetPointsWithResource(*world, pos, type); - return pts.empty() ? MapPoint::Invalid() : pts.front(); -} - bool nofMiner::AreWaresAvailable() const { if(!nofWorkman::AreWaresAvailable()) return false; - const MineResourceBehavior effectiveBehavior = - GetEffectiveMineResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); + const MineResourceBehavior behavior = GetMineResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); + if(behavior == MineResourceBehavior::WorkEverywhere) + return true; - if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() - || CanCreateWorkEverywhereResource(*world, pos, effectiveBehavior)) + if(FindPointWithResource(GetRequiredResType(), false).isValid()) return true; workplace->OnOutOfResources(); @@ -228,20 +201,15 @@ bool nofMiner::AreWaresAvailable() const bool nofMiner::StartWorking() { const GlobalGameSettings& settings = world->GetGGS(); - const MineResourceBehavior effectiveBehavior = - GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); - MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); + const MineResourceBehavior behavior = GetMineResourceBehavior(settings, workplace->GetBuildingType()); + if(behavior == MineResourceBehavior::WorkEverywhere) + return nofWorkman::StartWorking(); + + const MapPoint resPt = FindPointWithResource(GetRequiredResType()); if(!resPt.isValid()) - { - resPt = CreateWorkEverywhereResource(*world, pos, GetRequiredResType(), effectiveBehavior, GetObjId()); - if(!resPt.isValid()) - { - workplace->OnOutOfResources(); - return false; - } - } + return false; - if(effectiveBehavior != MineResourceBehavior::S4LikeExhaustion + if(behavior != MineResourceBehavior::S4LikeExhaustion && IsMineResourceDepletable(settings, workplace->GetBuildingType())) world->ReduceResource(resPt); diff --git a/libs/s25main/figures/nofMiner.h b/libs/s25main/figures/nofMiner.h index b13cf9a736..c3fdc810ad 100644 --- a/libs/s25main/figures/nofMiner.h +++ b/libs/s25main/figures/nofMiner.h @@ -23,7 +23,6 @@ class nofMiner : public nofWorkman bool AreWaresAvailable() const override; bool StartWorking() override; ResourceType GetRequiredResType() const; - MapPoint FindPointWithResourceQuiet(ResourceType type) const; public: nofMiner(MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/libs/s25main/figures/nofWorkman.cpp b/libs/s25main/figures/nofWorkman.cpp index 889ef38cb6..89f4025a5c 100644 --- a/libs/s25main/figures/nofWorkman.cpp +++ b/libs/s25main/figures/nofWorkman.cpp @@ -94,7 +94,7 @@ struct NodeHasResource }; } // namespace -MapPoint nofWorkman::FindPointWithResource(ResourceType type) const +MapPoint nofWorkman::FindPointWithResource(ResourceType type, const bool notify) const { // Alle Punkte durchgehen, bis man einen findet, wo man graben kann const std::vector pts = @@ -102,7 +102,8 @@ MapPoint nofWorkman::FindPointWithResource(ResourceType type) const if(!pts.empty()) return pts.front(); - workplace->OnOutOfResources(); + if(notify) + workplace->OnOutOfResources(); return MapPoint::Invalid(); } diff --git a/libs/s25main/figures/nofWorkman.h b/libs/s25main/figures/nofWorkman.h index 850b7a3808..eb730a4be5 100644 --- a/libs/s25main/figures/nofWorkman.h +++ b/libs/s25main/figures/nofWorkman.h @@ -37,7 +37,7 @@ class nofWorkman : public nofBuildingWorker virtual bool StartWorking(); /// Looks for a point with a given resource on the node - MapPoint FindPointWithResource(ResourceType type) const; + MapPoint FindPointWithResource(ResourceType type, bool notify = true) const; public: nofWorkman(Job job, MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 7b41468c14..de3937e02c 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -11,7 +11,8 @@ namespace { constexpr unsigned MAX_PRODUCTION_PERCENT = 100; -constexpr unsigned S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE = 5; +/// S4-like mines scale output chance against a 20-resource full-productivity reference capacity. +constexpr unsigned S4LIKE_RESOURCE_AMOUNT_FOR_FULL_PRODUCTION = 20; } // namespace AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) @@ -21,7 +22,7 @@ AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) case BuildingType::GoldMine: return AddonId::GOLDMINE_RESOURCE_BEHAVIOR; case BuildingType::IronMine: return AddonId::IRONMINE_RESOURCE_BEHAVIOR; case BuildingType::CoalMine: return AddonId::COALMINE_RESOURCE_BEHAVIOR; - default: return AddonId::GRANITEMINE_RESOURCE_BEHAVIOR; + default: return AddonId::INEXHAUSTIBLE_GRANITEMINES; } } @@ -36,53 +37,37 @@ ResourceType GetMineResourceType(const BuildingType buildingType) } } +helpers::OptionalEnum GetMineBuildingType(const ResourceType resourceType) +{ + switch(resourceType) + { + case ResourceType::Gold: return BuildingType::GoldMine; + case ResourceType::Iron: return BuildingType::IronMine; + case ResourceType::Coal: return BuildingType::CoalMine; + case ResourceType::Granite: return BuildingType::GraniteMine; + default: return boost::none; + } +} + unsigned GetS4LikeMineProductionChance(const unsigned resourceAmount) { - return std::min(MAX_PRODUCTION_PERCENT, resourceAmount * S4LIKE_PRODUCTION_PERCENT_PER_RESOURCE); + return std::min(MAX_PRODUCTION_PERCENT, + resourceAmount * MAX_PRODUCTION_PERCENT / S4LIKE_RESOURCE_AMOUNT_FOR_FULL_PRODUCTION); } -MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, - const BuildingType buildingType) +MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) { switch(static_cast(settings.getSelection(GetMineResourceBehaviorAddonId(buildingType)))) { - case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; case MineResourceBehavior::Inexhaustible: return MineResourceBehavior::Inexhaustible; + case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; case MineResourceBehavior::WorkEverywhere: return MineResourceBehavior::WorkEverywhere; default: return MineResourceBehavior::Default; } } -MineResourceBehavior GetEffectiveMineResourceBehavior(const GlobalGameSettings& settings, - const BuildingType buildingType) -{ - const MineResourceBehavior configuredBehavior = GetConfiguredMineResourceBehavior(settings, buildingType); - if(configuredBehavior != MineResourceBehavior::Default) - return configuredBehavior; - - if(buildingType == BuildingType::GraniteMine && settings.isEnabled(AddonId::GRANITEMINES_WORK_EVERYWHERE)) - return MineResourceBehavior::WorkEverywhere; - - if(settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) - return MineResourceBehavior::Inexhaustible; - - return MineResourceBehavior::Default; -} - bool IsMineResourceDepletable(const GlobalGameSettings& settings, const BuildingType buildingType) { - const MineResourceBehavior configuredBehavior = GetConfiguredMineResourceBehavior(settings, buildingType); - const MineResourceBehavior effectiveBehavior = GetEffectiveMineResourceBehavior(settings, buildingType); - - if(effectiveBehavior == MineResourceBehavior::Inexhaustible) - return false; - - if(configuredBehavior == MineResourceBehavior::Default && settings.isEnabled(AddonId::INEXHAUSTIBLE_MINES)) - return false; - - if(configuredBehavior == MineResourceBehavior::Default && buildingType == BuildingType::GraniteMine - && settings.isEnabled(AddonId::INEXHAUSTIBLE_GRANITEMINES)) - return false; - - return true; + const MineResourceBehavior behavior = GetMineResourceBehavior(settings, buildingType); + return behavior == MineResourceBehavior::Default || behavior == MineResourceBehavior::S4LikeExhaustion; } diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index 0e6914e00d..923cb2137e 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -4,6 +4,7 @@ #pragma once +#include "helpers/OptionalEnum.h" #include class GlobalGameSettings; @@ -13,15 +14,15 @@ enum class ResourceType : uint8_t; enum class MineResourceBehavior : unsigned { - Default, - S4LikeExhaustion, - Inexhaustible, - WorkEverywhere + Default = 0, + Inexhaustible = 1, + S4LikeExhaustion = 2, + WorkEverywhere = 3 }; AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); ResourceType GetMineResourceType(BuildingType buildingType); +helpers::OptionalEnum GetMineBuildingType(ResourceType resourceType); unsigned GetS4LikeMineProductionChance(unsigned resourceAmount); -MineResourceBehavior GetConfiguredMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); -MineResourceBehavior GetEffectiveMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); +MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); bool IsMineResourceDepletable(const GlobalGameSettings& settings, BuildingType buildingType); diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 358927f6a4..9279b7d9e5 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -170,14 +170,18 @@ BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingAIResourceOnly, EmptyWo makeMineNodesUsableForSearch(ai, world, 0); const MapPoint around = world.GetPlayer(0).GetHQPos(); + const MapPoint otherResourcePos = world.MakeMapPoint(around + Position(8, 0)); + world.GetNodeWriteable(otherResourcePos).resources = Resource(ResourceType::Iron, 4); + BOOST_TEST(ai.getAIInterface().GetResourceRating(otherResourcePos, AIResource::Coal) + == static_cast(RES_RADIUS[AIResource::Coal])); BOOST_TEST(ai.FindBestPosition(around, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); BOOST_TEST(!ai.FindBestPosition(around, AIResource::Ironore, BuildingQuality::Mine, 5).isValid()); } -BOOST_FIXTURE_TEST_CASE(GraniteLegacyWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) +BOOST_FIXTURE_TEST_CASE(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) { makeWorldMineable(world); - ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); makeMineNodesUsableForSearch(ai, world, 0); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index df6a8b3c89..53d6777fa2 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -198,40 +198,29 @@ BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteM BOOST_TEST(curInventory[GoodType::Stones] == initialStones); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereCreatesDepletableResource, GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource, GraniteMineWithoutResourcesFixture) { - ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); - BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); - - RTTR_EXEC_TILL(50000, world.GetNode(minePos).resources.getType() == ResourceType::Granite - && world.GetNode(minePos).resources.getAmount() == 0u); BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) - == static_cast(ResourceType::Granite)); + == static_cast(ResourceType::Nothing)); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereResourceIsInexhaustibleWithGraniteAddon, - GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereIgnoresExistingResource, MineProductionFixture) { - ggs.setSelection(AddonId::GRANITEMINES_WORK_EVERYWHERE, 1); - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); - const MapPoint minePos = CreateGraniteMineWithoutResources(); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); + const MapPoint minePos = CreateMine(BuildingType::GraniteMine, Resource(ResourceType::Coal, 4)); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); - BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); - const unsigned initialResourceAmount = world.GetNode(minePos).resources.getAmount(); - - RTTR_SKIP_GFS(10000); - - BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Granite)); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == initialResourceAmount); + BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Coal)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 4u); } BOOST_FIXTURE_TEST_CASE(CoalMineInexhaustibleBehaviorDoesNotDepleteResource, MineProductionFixture) @@ -373,7 +362,7 @@ BOOST_FIXTURE_TEST_CASE(CoalMineDefaultProductionIgnoresNoOutputFallback, MinePr BOOST_TEST(world.GetNode(minePos).resources.getAmount() < 3u); } -BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorCreatesDepletableResource, MineProductionFixture) +BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorProducesWithoutCreatingResource, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateMine(BuildingType::CoalMine); @@ -381,10 +370,8 @@ BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorCreatesDepletableResource, const unsigned initialCoal = curInventory[GoodType::Coal]; RTTR_EXEC_TILL(2000, curInventory[GoodType::Coal] > initialCoal); - BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Coal)); - - RTTR_EXEC_TILL(50000, world.GetNode(minePos).resources.getType() == ResourceType::Coal - && world.GetNode(minePos).resources.getAmount() == 0u); + BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) + == static_cast(ResourceType::Nothing)); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); } diff --git a/tests/s25Main/integration/testSerialization.cpp b/tests/s25Main/integration/testSerialization.cpp index 0b06c034b4..06844a2609 100644 --- a/tests/s25Main/integration/testSerialization.cpp +++ b/tests/s25Main/integration/testSerialization.cpp @@ -5,6 +5,7 @@ #include "GameCommands.h" #include "GameEvent.h" #include "GamePlayer.h" +#include "GlobalGameSettings.h" #include "PointOutput.h" #include "Replay.h" #include "RttrForeachPt.h" @@ -19,6 +20,7 @@ #include "figures/nofHunter.h" #include "helpers/OptionalIO.h" #include "helpers/format.hpp" +#include "helpers/serializeEnums.h" #include "network/GameMessage_Chat.h" #include "network/PlayerGameCommands.h" #include "worldFixtures/CreateEmptyWorld.h" @@ -30,6 +32,7 @@ #include "nodeObjs/noFlag.h" #include "gameTypes/GameTypesOutput.h" #include "gameTypes/MapInfo.h" +#include "gameTypes/MineResourceBehavior.h" #include "s25util/tmpFile.h" #include #include @@ -40,6 +43,7 @@ // LCOV_EXCL_START BOOST_TEST_DONT_PRINT_LOG_VALUE(Resource) BOOST_TEST_DONT_PRINT_LOG_VALUE(AddonId) +BOOST_TEST_DONT_PRINT_LOG_VALUE(MineResourceBehavior) BOOST_TEST_DONT_PRINT_LOG_VALUE(nofBuildingWorker::State) // LCOV_EXCL_STOP @@ -159,6 +163,17 @@ void CheckReplayCmds(Replay& loadReplay, const PlayerGameCommands& recordedCmds) gf = loadReplay.ReadGF(); BOOST_TEST(!gf); } + +void PushSerializedGGSHeader(Serializer& ser) +{ + helpers::pushEnum(ser, GameSpeed::Normal); + helpers::pushEnum(ser, GameObjective::None); + helpers::pushEnum(ser, StartWares::Normal); + ser.PushBool(false); + helpers::pushEnum(ser, Exploration::FogOfWar); + ser.PushBool(true); + ser.PushBool(false); +} } // namespace BOOST_AUTO_TEST_SUITE(Serialization) @@ -201,6 +216,44 @@ BOOST_AUTO_TEST_CASE(SerializeGGS) } } +BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeMigratesToPerMineBehaviors) +{ + Serializer ser; + PushSerializedGGSHeader(ser); + ser.PushUnsignedInt(1); + ser.PushUnsignedInt(static_cast(AddonId::INEXHAUSTIBLE_MINES)); + ser.PushUnsignedInt(1); + + Serializer loader(ser.GetData(), ser.GetLength()); + GlobalGameSettings ggsLoaded; + ggsLoaded.Deserialize(loader); + + for(const BuildingType mineType : + {BuildingType::GraniteMine, BuildingType::CoalMine, BuildingType::IronMine, BuildingType::GoldMine}) + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, mineType) == MineResourceBehavior::Inexhaustible); + BOOST_TEST(ggsLoaded.getSelection(AddonId::INEXHAUSTIBLE_MINES) == 0u); +} + +BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeDoesNotOverridePerMineBehavior) +{ + Serializer ser; + PushSerializedGGSHeader(ser); + ser.PushUnsignedInt(2); + ser.PushUnsignedInt(static_cast(AddonId::INEXHAUSTIBLE_MINES)); + ser.PushUnsignedInt(1); + ser.PushUnsignedInt(static_cast(AddonId::COALMINE_RESOURCE_BEHAVIOR)); + ser.PushUnsignedInt(static_cast(MineResourceBehavior::S4LikeExhaustion)); + + Serializer loader(ser.GetData(), ser.GetLength()); + GlobalGameSettings ggsLoaded; + ggsLoaded.Deserialize(loader); + + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, BuildingType::CoalMine) == MineResourceBehavior::S4LikeExhaustion); + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, BuildingType::GraniteMine) == MineResourceBehavior::Inexhaustible); + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, BuildingType::IronMine) == MineResourceBehavior::Inexhaustible); + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, BuildingType::GoldMine) == MineResourceBehavior::Inexhaustible); +} + BOOST_FIXTURE_TEST_CASE(BaseSaveLoad, RandWorldFixture) { MockLocalGameState lgsGame; From 86587eb6eac148beb41e62b914cd30d4681ec6aa Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 26 May 2026 18:29:51 +0200 Subject: [PATCH 12/23] Clarify S4-like mine productivity reference --- libs/s25main/gameTypes/MineResourceBehavior.cpp | 17 ++++++++++++----- libs/s25main/gameTypes/MineResourceBehavior.h | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index de3937e02c..a9650e6ede 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -11,8 +11,7 @@ namespace { constexpr unsigned MAX_PRODUCTION_PERCENT = 100; -/// S4-like mines scale output chance against a 20-resource full-productivity reference capacity. -constexpr unsigned S4LIKE_RESOURCE_AMOUNT_FOR_FULL_PRODUCTION = 20; +constexpr unsigned S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT = 20; } // namespace AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) @@ -49,10 +48,18 @@ helpers::OptionalEnum GetMineBuildingType(const ResourceType resou } } -unsigned GetS4LikeMineProductionChance(const unsigned resourceAmount) +unsigned GetS4LikeMineFullProductivityResourceAmount() { - return std::min(MAX_PRODUCTION_PERCENT, - resourceAmount * MAX_PRODUCTION_PERCENT / S4LIKE_RESOURCE_AMOUNT_FOR_FULL_PRODUCTION); + return S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT; +} + +unsigned GetS4LikeMineProductionChance(const unsigned remainingMatchingResources) +{ + // S4-like productivity is intentionally based on a 20-resource reference amount, not the theoretical maximum + // resources in the mine radius. Below that amount, the production chance degrades linearly. + const unsigned chancePercent = + remainingMatchingResources * MAX_PRODUCTION_PERCENT / GetS4LikeMineFullProductivityResourceAmount(); + return std::min(MAX_PRODUCTION_PERCENT, chancePercent); } MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index 923cb2137e..b86f03fc73 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -23,6 +23,8 @@ enum class MineResourceBehavior : unsigned AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); ResourceType GetMineResourceType(BuildingType buildingType); helpers::OptionalEnum GetMineBuildingType(ResourceType resourceType); -unsigned GetS4LikeMineProductionChance(unsigned resourceAmount); +/// Remaining matching resources at which S4-like mines reach full productivity. +unsigned GetS4LikeMineFullProductivityResourceAmount(); +unsigned GetS4LikeMineProductionChance(unsigned remainingMatchingResources); MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); bool IsMineResourceDepletable(const GlobalGameSettings& settings, BuildingType buildingType); From 9535c6c733aad01eafdb8e410a30f104a7060a0f Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 23 Jun 2026 16:41:53 +0200 Subject: [PATCH 13/23] Address mine resource behavior review feedback --- libs/common/include/helpers/MaxEnumValue.h | 7 +++++ libs/s25main/BuildingRegister.cpp | 15 ----------- libs/s25main/BuildingRegister.h | 2 -- libs/s25main/addons/const_addons.h | 3 ++- libs/s25main/buildings/nobUsual.cpp | 12 ++------- libs/s25main/buildings/nobUsual.h | 3 +-- libs/s25main/figures/nofMiner.cpp | 27 +++++-------------- libs/s25main/gameTypes/MineNoOutputFallback.h | 9 ++++++- .../gameTypes/MineResourceBehavior.cpp | 25 ++++++++++++----- libs/s25main/gameTypes/MineResourceBehavior.h | 17 ++++++++---- libs/s25main/ingameWindows/iwBuilding.cpp | 8 +++--- libs/s25main/ingameWindows/iwBuilding.h | 2 +- .../iwBuildingProductivities.cpp | 2 +- tests/s25Main/integration/testGamePlayer.cpp | 12 ++++----- 14 files changed, 68 insertions(+), 76 deletions(-) diff --git a/libs/common/include/helpers/MaxEnumValue.h b/libs/common/include/helpers/MaxEnumValue.h index 4593eff7e4..4f6a99e33d 100644 --- a/libs/common/include/helpers/MaxEnumValue.h +++ b/libs/common/include/helpers/MaxEnumValue.h @@ -30,4 +30,11 @@ inline constexpr unsigned MaxEnumValue_v = static_cast inline constexpr unsigned NumEnumValues_v = MaxEnumValue_v + 1u; +/// Check whether a numeric value is a valid enumerator for an enum type +template +inline constexpr bool isValidEnumValue(unsigned value) +{ + return value <= MaxEnumValue_v; +} + } // namespace helpers diff --git a/libs/s25main/BuildingRegister.cpp b/libs/s25main/BuildingRegister.cpp index c26c8a6162..8ae1a30a5a 100644 --- a/libs/s25main/BuildingRegister.cpp +++ b/libs/s25main/BuildingRegister.cpp @@ -170,21 +170,6 @@ helpers::EnumArray BuildingRegister::CalcProductivities( return productivities; } -helpers::EnumArray BuildingRegister::CalcDisplayProductivities() const -{ - helpers::EnumArray productivities; - - for(const auto bld : helpers::enumRange()) - { - if(holds_alternative(BLD_WORK_DESC[bld].producedWare)) - productivities[bld] = 0; - else - productivities[bld] = - static_cast(::CalcAverageProductivity(GetBuildings(bld), &nobUsual::GetDisplayProductivity)); - } - return productivities; -} - unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const { if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) diff --git a/libs/s25main/BuildingRegister.h b/libs/s25main/BuildingRegister.h index 003133e130..bcc5fb488c 100644 --- a/libs/s25main/BuildingRegister.h +++ b/libs/s25main/BuildingRegister.h @@ -41,8 +41,6 @@ class BuildingRegister BuildingCount GetBuildingNums() const; /// Calculate and fill the average productivities for all buildings. helpers::EnumArray CalcProductivities() const; - /// Calculate and fill the average productivities shown in UI. - helpers::EnumArray CalcDisplayProductivities() const; /// Calculate the average productivity for a building type unsigned CalcAverageProductivity(BuildingType bldType) const; /// Calculate the average productivity for all buildings diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 89db9e5622..3aab335fa8 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -38,7 +38,8 @@ // // Add the #include for your AddonXXX.h in Addons.h! // -// TODO: INEXHAUSTIBLE_MINES is kept only for loading old settings/savegames until the gamedata version can be raised. +// TODO(Savegame): Remove INEXHAUSTIBLE_MINES once the gamedata version can be raised and legacy settings/savegames no +// longer need migration. ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x00000001, REFUND_MATERIALS = 0x00000002, EXHAUSTIBLE_WATER = 0x00000003, REFUND_ON_EMERGENCY = 0x00000004, MANUAL_ROAD_ENLARGEMENT = 0x00000005, CATAPULT_GRAPHICS = 0x00000006, METALWORKSBEHAVIORONZERO = 0x00000007, diff --git a/libs/s25main/buildings/nobUsual.cpp b/libs/s25main/buildings/nobUsual.cpp index 39ff3acddd..32db3a2883 100644 --- a/libs/s25main/buildings/nobUsual.cpp +++ b/libs/s25main/buildings/nobUsual.cpp @@ -522,21 +522,13 @@ bool nobUsual::HasWorker() const return worker && worker->GetState() != nofBuildingWorker::State::FigureWork; } -unsigned short nobUsual::GetDisplayProductivity() const +unsigned short nobUsual::GetProductivity() const { if(!BuildingProperties::IsMine(bldType_) || GetMineResourceBehavior(world->GetGGS(), bldType_) != MineResourceBehavior::S4LikeExhaustion) return productivity; - const ResourceType resourceType = GetMineResourceType(bldType_); - const std::vector resourcePts = world->GetMatchingPointsInRadius<1>( - pos, MINER_RADIUS, - [this, resourceType](const MapPoint pt) { return world->GetNode(pt).resources.has(resourceType); }, true); - - unsigned resourceAmount = 0; - for(const MapPoint pt : resourcePts) - resourceAmount += world->GetNode(pt).resources.getAmount(); - + const unsigned resourceAmount = GetRemainingMineResources(*world, pos, GetMineResourceType(bldType_)); return static_cast( (static_cast(productivity) * GetS4LikeMineProductionChance(resourceAmount)) / 100u); } diff --git a/libs/s25main/buildings/nobUsual.h b/libs/s25main/buildings/nobUsual.h index 436c61c8a7..a128c09299 100644 --- a/libs/s25main/buildings/nobUsual.h +++ b/libs/s25main/buildings/nobUsual.h @@ -101,8 +101,7 @@ class nobUsual : public noBuilding /// Gibt Pointer auf Produktivität zurück const unsigned short* GetProductivityPointer() const { return &productivity; } - unsigned short GetProductivity() const { return productivity; } - unsigned short GetDisplayProductivity() const; + unsigned short GetProductivity() const; const nofBuildingWorker* GetWorker() const { return worker; } /// Stoppt/Erlaubt Produktion (visuell) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 2cfe2af638..c1dcf5fb74 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -27,24 +27,10 @@ constexpr unsigned S4LIKE_MIN_RESOURCE_AMOUNT = 1; MineNoOutputFallback GetConfiguredNoOutputFallback(const GlobalGameSettings& settings) { - switch(static_cast(settings.getSelection(AddonId::MINE_NO_OUTPUT_FALLBACK))) - { - case MineNoOutputFallback::ProduceGranite25: return MineNoOutputFallback::ProduceGranite25; - case MineNoOutputFallback::ProduceGranite50: return MineNoOutputFallback::ProduceGranite50; - case MineNoOutputFallback::ProduceGranite100: return MineNoOutputFallback::ProduceGranite100; - case MineNoOutputFallback::ProduceLowerGradeResource: return MineNoOutputFallback::ProduceLowerGradeResource; - default: return MineNoOutputFallback::ProduceNothing; - } -} - -unsigned GetS4LikeProductionChanceForRemainingResources(const GameWorld& world, - const std::vector& resourcePts) -{ - unsigned resourceAmount = 0; - for(const MapPoint pt : resourcePts) - resourceAmount += world.GetNode(pt).resources.getAmount(); - - return GetS4LikeMineProductionChance(resourceAmount); + const unsigned selection = settings.getSelection(AddonId::MINE_NO_OUTPUT_FALLBACK); + if(!helpers::isValidEnumValue(selection)) + return MineNoOutputFallback::ProduceNothing; + return static_cast(selection); } unsigned GetGraniteFallbackChance(const MineNoOutputFallback fallback) @@ -164,8 +150,9 @@ helpers::OptionalEnum nofMiner::ProduceWare() { const std::vector resourcePts = GetPointsWithResource(*world, pos, GetRequiredResType()); const auto productionRoll = static_cast(RANDOM_RAND(MAX_PRODUCTION_PERCENT)); - const bool produceNothingThisCycle = - resourcePts.empty() || productionRoll >= GetS4LikeProductionChanceForRemainingResources(*world, resourcePts); + const bool produceNothingThisCycle = resourcePts.empty() + || productionRoll >= GetS4LikeMineProductionChance( + GetRemainingMineResources(*world, pos, GetRequiredResType())); if(produceNothingThisCycle) return GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); diff --git a/libs/s25main/gameTypes/MineNoOutputFallback.h b/libs/s25main/gameTypes/MineNoOutputFallback.h index 7d0d9dc966..1e3d4dc832 100644 --- a/libs/s25main/gameTypes/MineNoOutputFallback.h +++ b/libs/s25main/gameTypes/MineNoOutputFallback.h @@ -4,7 +4,9 @@ #pragma once -enum class MineNoOutputFallback : unsigned +#include "helpers/MaxEnumValue.h" + +enum class MineNoOutputFallback { ProduceNothing, ProduceGranite25, @@ -12,3 +14,8 @@ enum class MineNoOutputFallback : unsigned ProduceGranite100, ProduceLowerGradeResource }; + +constexpr auto maxEnumValue(MineNoOutputFallback) +{ + return MineNoOutputFallback::ProduceLowerGradeResource; +} diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index a9650e6ede..8b7bf02405 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -5,8 +5,11 @@ #include "MineResourceBehavior.h" #include "GlobalGameSettings.h" #include "addons/const_addons.h" +#include "world/GameWorld.h" #include "gameTypes/BuildingType.h" +#include "gameTypes/MapCoordinates.h" #include "gameTypes/Resource.h" +#include "gameData/GameConsts.h" #include namespace { @@ -48,6 +51,17 @@ helpers::OptionalEnum GetMineBuildingType(const ResourceType resou } } +unsigned GetRemainingMineResources(const GameWorld& world, const MapPoint pos, const ResourceType resourceType) +{ + unsigned resourceAmount = 0; + const auto resourcePts = world.GetMatchingPointsInRadius<1>( + pos, MINER_RADIUS, + [&world, resourceType](const MapPoint pt) { return world.GetNode(pt).resources.has(resourceType); }, true); + for(const MapPoint pt : resourcePts) + resourceAmount += world.GetNode(pt).resources.getAmount(); + return resourceAmount; +} + unsigned GetS4LikeMineFullProductivityResourceAmount() { return S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT; @@ -64,13 +78,10 @@ unsigned GetS4LikeMineProductionChance(const unsigned remainingMatchingResources MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, const BuildingType buildingType) { - switch(static_cast(settings.getSelection(GetMineResourceBehaviorAddonId(buildingType)))) - { - case MineResourceBehavior::Inexhaustible: return MineResourceBehavior::Inexhaustible; - case MineResourceBehavior::S4LikeExhaustion: return MineResourceBehavior::S4LikeExhaustion; - case MineResourceBehavior::WorkEverywhere: return MineResourceBehavior::WorkEverywhere; - default: return MineResourceBehavior::Default; - } + const unsigned selection = settings.getSelection(GetMineResourceBehaviorAddonId(buildingType)); + if(!helpers::isValidEnumValue(selection)) + return MineResourceBehavior::Default; + return static_cast(selection); } bool IsMineResourceDepletable(const GlobalGameSettings& settings, const BuildingType buildingType) diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index b86f03fc73..e1457dd76e 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -5,24 +5,31 @@ #pragma once #include "helpers/OptionalEnum.h" +#include "gameTypes/MapCoordinates.h" #include class GlobalGameSettings; +class GameWorld; enum class AddonId; enum class BuildingType : unsigned char; enum class ResourceType : uint8_t; -enum class MineResourceBehavior : unsigned +enum class MineResourceBehavior { - Default = 0, - Inexhaustible = 1, - S4LikeExhaustion = 2, - WorkEverywhere = 3 + Default, + Inexhaustible, + S4LikeExhaustion, + WorkEverywhere }; +constexpr auto maxEnumValue(MineResourceBehavior) +{ + return MineResourceBehavior::WorkEverywhere; +} AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); ResourceType GetMineResourceType(BuildingType buildingType); helpers::OptionalEnum GetMineBuildingType(ResourceType resourceType); +unsigned GetRemainingMineResources(const GameWorld& world, MapPoint pos, ResourceType resourceType); /// Remaining matching resources at which S4-like mines reach full productivity. unsigned GetS4LikeMineFullProductivityResourceAmount(); unsigned GetS4LikeMineProductionChance(unsigned remainingMatchingResources); diff --git a/libs/s25main/ingameWindows/iwBuilding.cpp b/libs/s25main/ingameWindows/iwBuilding.cpp index aac62f5dc4..28cd3ba4c4 100644 --- a/libs/s25main/ingameWindows/iwBuilding.cpp +++ b/libs/s25main/ingameWindows/iwBuilding.cpp @@ -32,7 +32,7 @@ const unsigned IODAT_SHIP_ID = 218; iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsual* const building, Extent extent) : IngameWindow(CGI_BUILDING + MapBase::CreateGUIID(building->GetPos()), IngameWindow::posAtMouse, extent, _(BUILDING_NAMES[building->GetBuildingType()]), LOADER.GetImageN("resource", 41)), - gwv(gwv), gcFactory(gcFactory), building(building), displayProductivity(building->GetDisplayProductivity()) + gwv(gwv), gcFactory(gcFactory), building(building), productivity(building->GetProductivity()) { // Arbeitersymbol AddImage(0, DrawPoint(28, 39), LOADER.GetMapTexture(2298)); @@ -83,8 +83,8 @@ iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsu _("Go to place")); // Produktivitätsanzeige (bei Katapulten und Spähtürmen ausblenden) - Window* productivity = AddPercent(9, DrawPoint(59, 31), Extent(106, 16), TextureColor::Grey, 0xFFFFFF00, SmallFont, - &displayProductivity); + Window* productivity = + AddPercent(9, DrawPoint(59, 31), Extent(106, 16), TextureColor::Grey, 0xFFFFFF00, SmallFont, &this->productivity); if(building->GetBuildingType() == BuildingType::Catapult || building->GetBuildingType() == BuildingType::LookoutTower) productivity->SetVisible(false); @@ -99,7 +99,7 @@ iwBuilding::iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsu void iwBuilding::Msg_PaintBefore() { IngameWindow::Msg_PaintBefore(); - displayProductivity = building->GetDisplayProductivity(); + productivity = building->GetProductivity(); // Haus unbesetzt ggf ausblenden GetCtrl(10)->SetVisible(!building->HasWorker()); diff --git a/libs/s25main/ingameWindows/iwBuilding.h b/libs/s25main/ingameWindows/iwBuilding.h index 96daa9bd2c..03dce9dab9 100644 --- a/libs/s25main/ingameWindows/iwBuilding.h +++ b/libs/s25main/ingameWindows/iwBuilding.h @@ -16,7 +16,7 @@ class iwBuilding : public IngameWindow GameWorldView& gwv; GameCommandFactory& gcFactory; nobUsual* const building; /// Das zugehörige Gebäudeobjekt - unsigned short displayProductivity; + unsigned short productivity; public: iwBuilding(GameWorldView& gwv, GameCommandFactory& gcFactory, nobUsual* building, Extent extent = Extent(226, 194)); diff --git a/libs/s25main/ingameWindows/iwBuildingProductivities.cpp b/libs/s25main/ingameWindows/iwBuildingProductivities.cpp index 4f5e04d605..af44722c60 100644 --- a/libs/s25main/ingameWindows/iwBuildingProductivities.cpp +++ b/libs/s25main/ingameWindows/iwBuildingProductivities.cpp @@ -100,7 +100,7 @@ iwBuildingProductivities::iwBuildingProductivities(const GamePlayer& player) void iwBuildingProductivities::UpdatePercents() { - percents = player.GetBuildingRegister().CalcDisplayProductivities(); + percents = player.GetBuildingRegister().CalcProductivities(); } void iwBuildingProductivities::Msg_PaintAfter() diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index cc9dc96f65..7a28d58eff 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -123,7 +123,7 @@ BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) BOOST_TEST(buildingRegister.CalcAverageProductivity() == avgProd); } -BOOST_FIXTURE_TEST_CASE(MineDisplayProductivityAccountsForS4LikeResourceChance, WorldFixtureEmpty1P) +BOOST_FIXTURE_TEST_CASE(MineProductivityAccountsForS4LikeResourceChance, WorldFixtureEmpty1P) { MapPoint minePos(0, 0); while(world.GetNode(minePos).bq != BuildingQuality::Castle) @@ -135,21 +135,19 @@ BOOST_FIXTURE_TEST_CASE(MineDisplayProductivityAccountsForS4LikeResourceChance, world.SetResource(minePos, Resource(ResourceType::Coal, 1)); BOOST_TEST(coalMine->GetProductivity() == 100u); - BOOST_TEST(coalMine->GetDisplayProductivity() == 100u); ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::S4LikeExhaustion)); - BOOST_TEST(coalMine->GetProductivity() == 100u); - BOOST_TEST(coalMine->GetDisplayProductivity() == 5u); + BOOST_TEST(coalMine->GetProductivity() == 5u); setProductivity(coalMine, 80); world.SetResource(minePos, Resource(ResourceType::Coal, 10)); - BOOST_TEST(coalMine->GetDisplayProductivity() == 40u); - BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcDisplayProductivities()[BuildingType::CoalMine] == 40u); + BOOST_TEST(coalMine->GetProductivity() == 40u); + BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 40u); setProductivity(coalMine, 100); world.SetResource(minePos, Resource(ResourceType::Coal, 15)); - BOOST_TEST(coalMine->GetDisplayProductivity() == 75u); + BOOST_TEST(coalMine->GetProductivity() == 75u); } BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) From 1bade00db025eca96665bfe7090969cb5c52678e Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 23 Jun 2026 17:21:04 +0200 Subject: [PATCH 14/23] Fix mine resource behavior review regression --- libs/s25main/figures/nofMiner.cpp | 2 +- .../gameTypes/MineResourceBehavior.cpp | 2 +- tests/s25Main/integration/testGamePlayer.cpp | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index c1dcf5fb74..d3fc58fe33 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -78,7 +78,7 @@ helpers::OptionalEnum GetNoOutputFallbackGood(const GlobalGameSettings std::vector GetPointsWithResource(const GameWorld& world, const MapPoint pos, const ResourceType type) { - return world.GetMatchingPointsInRadius<1>( + return world.GetMatchingPointsInRadius( pos, MINER_RADIUS, [&world, type](const MapPoint pt) { return world.GetNode(pt).resources.has(type); }, true); } diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 8b7bf02405..4c694ca706 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -54,7 +54,7 @@ helpers::OptionalEnum GetMineBuildingType(const ResourceType resou unsigned GetRemainingMineResources(const GameWorld& world, const MapPoint pos, const ResourceType resourceType) { unsigned resourceAmount = 0; - const auto resourcePts = world.GetMatchingPointsInRadius<1>( + const auto resourcePts = world.GetMatchingPointsInRadius( pos, MINER_RADIUS, [&world, resourceType](const MapPoint pt) { return world.GetNode(pt).resources.has(resourceType); }, true); for(const MapPoint pt : resourcePts) diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 7a28d58eff..6eaf63d831 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -15,6 +15,7 @@ #include "gameTypes/MineResourceBehavior.h" #include "gameTypes/Resource.h" #include "gameData/BuildingProperties.h" +#include "gameData/GameConsts.h" #include "rttr/test/random.hpp" #include "s25util/warningSuppression.h" #include @@ -57,6 +58,7 @@ RTTR_ATTRIBUTE_NO_UBSAN(vptr) void setProductivity(nobUsual* bld, unsigned short } // namespace using WorldFixtureEmpty1P = WorldFixture + 14, 4>; +using WorldFixtureMineRadius1P = WorldFixture; BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) { using boost::test_tools::per_element; @@ -150,6 +152,48 @@ BOOST_FIXTURE_TEST_CASE(MineProductivityAccountsForS4LikeResourceChance, WorldFi BOOST_TEST(coalMine->GetProductivity() == 75u); } +BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius, WorldFixtureMineRadius1P) +{ + MapPoint minePos = MapPoint::Invalid(); + for(MapCoord y = MINER_RADIUS; y + MINER_RADIUS < world.GetSize().y && !minePos.isValid(); ++y) + { + for(MapCoord x = MINER_RADIUS; x + MINER_RADIUS < world.GetSize().x; ++x) + { + const MapPoint pt(x, y); + if(world.GetNode(pt).bq == BuildingQuality::Castle) + { + minePos = pt; + break; + } + } + } + BOOST_TEST_REQUIRE(minePos.isValid()); + + auto* coalMine = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::CoalMine, minePos, 0, Nation::Romans)); + setProductivity(coalMine, 100); + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::S4LikeExhaustion)); + + const auto inRangePts = world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS); + for(const MapPoint pt : inRangePts) + world.SetResource(pt, Resource()); + + const MapPoint westPt = world.GetNeighbour(minePos, Direction::West); + const MapPoint eastPt = world.GetNeighbour(minePos, Direction::East); + world.SetResource(westPt, Resource(ResourceType::Coal, 4)); + world.SetResource(eastPt, Resource(ResourceType::Coal, 6)); + BOOST_TEST(coalMine->GetProductivity() == 50u); + BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 50u); + + const MapPoint outOfRangePt = world.GetNeighbour( + world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); + const bool isOutOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); + BOOST_TEST_REQUIRE(isOutOfRange); + world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); + BOOST_TEST(coalMine->GetProductivity() == 50u); +} + BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) { GamePlayer& p1 = world.GetPlayer(0); From e17142e6b6607fbfc2a6245988fc01e0e5f4e3eb Mon Sep 17 00:00:00 2001 From: Manu Date: Tue, 23 Jun 2026 20:10:06 +0200 Subject: [PATCH 15/23] Harden mine resource behavior coverage --- tests/s25Main/integration/testGamePlayer.cpp | 57 +++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 6eaf63d831..6416de6191 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -59,6 +59,22 @@ RTTR_ATTRIBUTE_NO_UBSAN(vptr) void setProductivity(nobUsual* bld, unsigned short using WorldFixtureEmpty1P = WorldFixture + 14, 4>; using WorldFixtureMineRadius1P = WorldFixture; + +MapPoint FindMinePosition(const WorldFixtureMineRadius1P& fixture) +{ + for(MapCoord y = MINER_RADIUS; y + MINER_RADIUS < fixture.world.GetSize().y; ++y) + { + for(MapCoord x = MINER_RADIUS; x + MINER_RADIUS < fixture.world.GetSize().x; ++x) + { + const MapPoint pt(x, y); + if(fixture.world.GetNode(pt).bq == BuildingQuality::Castle) + return pt; + } + } + + return MapPoint::Invalid(); +} + BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) { using boost::test_tools::per_element; @@ -142,31 +158,31 @@ BOOST_FIXTURE_TEST_CASE(MineProductivityAccountsForS4LikeResourceChance, WorldFi static_cast(MineResourceBehavior::S4LikeExhaustion)); BOOST_TEST(coalMine->GetProductivity() == 5u); + world.SetResource(minePos, Resource(ResourceType::Coal, 15)); + world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource(ResourceType::Coal, 5)); + BOOST_TEST(coalMine->GetProductivity() == 100u); + setProductivity(coalMine, 80); world.SetResource(minePos, Resource(ResourceType::Coal, 10)); + world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource()); BOOST_TEST(coalMine->GetProductivity() == 40u); BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 40u); setProductivity(coalMine, 100); world.SetResource(minePos, Resource(ResourceType::Coal, 15)); + world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource()); BOOST_TEST(coalMine->GetProductivity() == 75u); + + world.SetResource(minePos, Resource()); + BOOST_TEST(coalMine->GetProductivity() == 0u); + + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); + BOOST_TEST(coalMine->GetProductivity() == 100u); } BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius, WorldFixtureMineRadius1P) { - MapPoint minePos = MapPoint::Invalid(); - for(MapCoord y = MINER_RADIUS; y + MINER_RADIUS < world.GetSize().y && !minePos.isValid(); ++y) - { - for(MapCoord x = MINER_RADIUS; x + MINER_RADIUS < world.GetSize().x; ++x) - { - const MapPoint pt(x, y); - if(world.GetNode(pt).bq == BuildingQuality::Castle) - { - minePos = pt; - break; - } - } - } + const MapPoint minePos = FindMinePosition(*this); BOOST_TEST_REQUIRE(minePos.isValid()); auto* coalMine = static_cast( @@ -179,19 +195,34 @@ BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius for(const MapPoint pt : inRangePts) world.SetResource(pt, Resource()); + BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 0u); + BOOST_TEST(coalMine->GetProductivity() == 0u); + const MapPoint westPt = world.GetNeighbour(minePos, Direction::West); const MapPoint eastPt = world.GetNeighbour(minePos, Direction::East); world.SetResource(westPt, Resource(ResourceType::Coal, 4)); world.SetResource(eastPt, Resource(ResourceType::Coal, 6)); + BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); BOOST_TEST(coalMine->GetProductivity() == 50u); BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 50u); + world.SetResource(world.GetNeighbour(minePos, Direction::NorthWest), Resource(ResourceType::Iron, 15)); + BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); + BOOST_TEST(coalMine->GetProductivity() == 50u); + const MapPoint outOfRangePt = world.GetNeighbour( world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); const bool isOutOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); BOOST_TEST_REQUIRE(isOutOfRange); world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); + BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); BOOST_TEST(coalMine->GetProductivity() == 50u); + + setProductivity(coalMine, 99); + world.SetResource(westPt, Resource(ResourceType::Coal, 5)); + world.SetResource(eastPt, Resource(ResourceType::Coal, 6)); + BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 11u); + BOOST_TEST(coalMine->GetProductivity() == 54u); } BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) From 2cb76605a36d899852f31dc1d90f8397d50b311e Mon Sep 17 00:00:00 2001 From: Manu Date: Thu, 25 Jun 2026 12:00:48 +0200 Subject: [PATCH 16/23] Polish mine resource behavior compatibility --- libs/s25main/addons/const_addons.h | 4 ++-- libs/s25main/gameTypes/MineResourceBehavior.cpp | 11 +++-------- libs/s25main/gameTypes/MineResourceBehavior.h | 2 -- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 3aab335fa8..8c0bcc5685 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -38,8 +38,8 @@ // // Add the #include for your AddonXXX.h in Addons.h! // -// TODO(Savegame): Remove INEXHAUSTIBLE_MINES once the gamedata version can be raised and legacy settings/savegames no -// longer need migration. +// TODO(Replay) TODO(Savegame): Remove INEXHAUSTIBLE_MINES once the gamedata version can be raised and legacy +// settings/savegames no longer need migration. ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x00000001, REFUND_MATERIALS = 0x00000002, EXHAUSTIBLE_WATER = 0x00000003, REFUND_ON_EMERGENCY = 0x00000004, MANUAL_ROAD_ENLARGEMENT = 0x00000005, CATAPULT_GRAPHICS = 0x00000006, METALWORKSBEHAVIORONZERO = 0x00000007, diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 4c694ca706..5a54ad2b23 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -14,6 +14,8 @@ namespace { constexpr unsigned MAX_PRODUCTION_PERCENT = 100; +// S4-like productivity reaches 100% at this remaining-resource amount instead of using the theoretical maximum +// resources in the mine radius. Below this reference amount, the production chance degrades linearly. constexpr unsigned S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT = 20; } // namespace @@ -62,17 +64,10 @@ unsigned GetRemainingMineResources(const GameWorld& world, const MapPoint pos, c return resourceAmount; } -unsigned GetS4LikeMineFullProductivityResourceAmount() -{ - return S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT; -} - unsigned GetS4LikeMineProductionChance(const unsigned remainingMatchingResources) { - // S4-like productivity is intentionally based on a 20-resource reference amount, not the theoretical maximum - // resources in the mine radius. Below that amount, the production chance degrades linearly. const unsigned chancePercent = - remainingMatchingResources * MAX_PRODUCTION_PERCENT / GetS4LikeMineFullProductivityResourceAmount(); + remainingMatchingResources * MAX_PRODUCTION_PERCENT / S4LIKE_FULL_PRODUCTIVITY_RESOURCE_AMOUNT; return std::min(MAX_PRODUCTION_PERCENT, chancePercent); } diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index e1457dd76e..5ca06f56a3 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -30,8 +30,6 @@ AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); ResourceType GetMineResourceType(BuildingType buildingType); helpers::OptionalEnum GetMineBuildingType(ResourceType resourceType); unsigned GetRemainingMineResources(const GameWorld& world, MapPoint pos, ResourceType resourceType); -/// Remaining matching resources at which S4-like mines reach full productivity. -unsigned GetS4LikeMineFullProductivityResourceAmount(); unsigned GetS4LikeMineProductionChance(unsigned remainingMatchingResources); MineResourceBehavior GetMineResourceBehavior(const GlobalGameSettings& settings, BuildingType buildingType); bool IsMineResourceDepletable(const GlobalGameSettings& settings, BuildingType buildingType); From 87ca9e6e8db856a18201b166dfba08f57337ba9d Mon Sep 17 00:00:00 2001 From: Manu Date: Mon, 20 Jul 2026 10:36:38 +0200 Subject: [PATCH 17/23] Address remaining mine resource review comments --- libs/s25main/GlobalGameSettings.cpp | 2 +- .../addons/AddonInexhaustibleGraniteMines.h | 23 ------------------- libs/s25main/addons/AddonInexhaustibleMines.h | 6 +++-- .../addons/AddonMineResourceBehavior.h | 15 ++++++++++++ libs/s25main/addons/Addons.h | 1 - libs/s25main/addons/const_addons.h | 2 +- libs/s25main/figures/nofMiner.cpp | 12 ++++++---- libs/s25main/figures/nofWorkman.cpp | 5 +--- libs/s25main/figures/nofWorkman.h | 2 +- .../gameTypes/MineResourceBehavior.cpp | 2 +- tests/s25Main/integration/testAI.cpp | 4 ++-- tests/s25Main/integration/testProduction.cpp | 6 ++--- 12 files changed, 36 insertions(+), 44 deletions(-) delete mode 100644 libs/s25main/addons/AddonInexhaustibleGraniteMines.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index ac8a70ff7f..e02cd5875a 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -107,7 +107,7 @@ void GlobalGameSettings::registerAllAddons() AddonFrontierDistanceReachable, AddonHalfCostMilEquip, AddonInexhaustibleFish, - AddonInexhaustibleGraniteMines, + AddonGraniteMineResourceBehavior, AddonCoalMineResourceBehavior, AddonIronMineResourceBehavior, AddonGoldMineResourceBehavior, diff --git a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h deleted file mode 100644 index 80c95ca8c3..0000000000 --- a/libs/s25main/addons/AddonInexhaustibleGraniteMines.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2005 - 2021 Settlers Freaks -// -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "AddonMineResourceBehavior.h" -#include "mygettext/mygettext.h" - -/** - * Granite mine resource behavior list. - * - * Reuses the old boolean INEXHAUSTIBLE_GRANITEMINES id: saved value 0 still means default behavior and saved value 1 - * now selects the inexhaustible behavior. - */ -class AddonInexhaustibleGraniteMines : public AddonMineResourceBehaviorBase -{ -public: - AddonInexhaustibleGraniteMines() - : AddonMineResourceBehaviorBase(AddonId::INEXHAUSTIBLE_GRANITEMINES, _("Granite Mine Resource Behavior"), - _("Configures how granite mines consume and exhaust stone deposits.")) - {} -}; diff --git a/libs/s25main/addons/AddonInexhaustibleMines.h b/libs/s25main/addons/AddonInexhaustibleMines.h index edb28f5b83..2265781915 100644 --- a/libs/s25main/addons/AddonInexhaustibleMines.h +++ b/libs/s25main/addons/AddonInexhaustibleMines.h @@ -10,9 +10,11 @@ /** * Deprecated global mine setting. * - * Not registered anymore. The ID is still decoded when loading old - * settings/savegames and migrated to the per-mine + * Not registered anymore. The ID is still decoded when loading old settings/savegames and migrated to the per-mine * resource behavior settings. + * + * TODO(Replay) TODO(Savegame): Remove this compatibility class when legacy global mine settings no longer need + * migration. */ class AddonInexhaustibleMines : public AddonBool { diff --git a/libs/s25main/addons/AddonMineResourceBehavior.h b/libs/s25main/addons/AddonMineResourceBehavior.h index 5ae0e9c960..ba6a356824 100644 --- a/libs/s25main/addons/AddonMineResourceBehavior.h +++ b/libs/s25main/addons/AddonMineResourceBehavior.h @@ -20,6 +20,21 @@ class AddonMineResourceBehaviorBase : public AddonList {} }; +/** + * Granite mine resource behavior list. + * + * Reuses the legacy granite-mine addon value 0x00800000: saved value 0 remains the default behavior and saved value 1 + * selects the inexhaustible behavior. + */ +class AddonGraniteMineResourceBehavior : public AddonMineResourceBehaviorBase +{ +public: + AddonGraniteMineResourceBehavior() + : AddonMineResourceBehaviorBase(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, _("Granite Mine Resource Behavior"), + _("Configures how granite mines consume and exhaust stone deposits.")) + {} +}; + class AddonCoalMineResourceBehavior : public AddonMineResourceBehaviorBase { public: diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 8bbc7c68c4..6f93c9226a 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -34,7 +34,6 @@ #include "addons/AddonToolOrdering.h" #include "addons/AddonInexhaustibleFish.h" -#include "addons/AddonInexhaustibleGraniteMines.h" #include "addons/AddonMaxRank.h" #include "addons/AddonMilitaryAid.h" #include "addons/AddonMineNoOutputFallback.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 8c0bcc5685..877f73ec38 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -59,7 +59,7 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, - INEXHAUSTIBLE_GRANITEMINES = 0x00800000, COALMINE_RESOURCE_BEHAVIOR = 0x00800001, + GRANITEMINE_RESOURCE_BEHAVIOR = 0x00800000, COALMINE_RESOURCE_BEHAVIOR = 0x00800001, IRONMINE_RESOURCE_BEHAVIOR = 0x00800002, GOLDMINE_RESOURCE_BEHAVIOR = 0x00800003, MINE_NO_OUTPUT_FALLBACK = 0x00800004, diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index d3fc58fe33..2b88cd0ebc 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -178,11 +178,10 @@ bool nofMiner::AreWaresAvailable() const if(behavior == MineResourceBehavior::WorkEverywhere) return true; - if(FindPointWithResource(GetRequiredResType(), false).isValid()) - return true; - - workplace->OnOutOfResources(); - return false; + const bool hasResources = FindPointWithResource(GetRequiredResType()).isValid(); + if(!hasResources) + workplace->OnOutOfResources(); + return hasResources; } bool nofMiner::StartWorking() @@ -194,7 +193,10 @@ bool nofMiner::StartWorking() const MapPoint resPt = FindPointWithResource(GetRequiredResType()); if(!resPt.isValid()) + { + workplace->OnOutOfResources(); return false; + } if(behavior != MineResourceBehavior::S4LikeExhaustion && IsMineResourceDepletable(settings, workplace->GetBuildingType())) diff --git a/libs/s25main/figures/nofWorkman.cpp b/libs/s25main/figures/nofWorkman.cpp index 89f4025a5c..a0a0e30d9e 100644 --- a/libs/s25main/figures/nofWorkman.cpp +++ b/libs/s25main/figures/nofWorkman.cpp @@ -94,7 +94,7 @@ struct NodeHasResource }; } // namespace -MapPoint nofWorkman::FindPointWithResource(ResourceType type, const bool notify) const +MapPoint nofWorkman::FindPointWithResource(ResourceType type) const { // Alle Punkte durchgehen, bis man einen findet, wo man graben kann const std::vector pts = @@ -102,8 +102,5 @@ MapPoint nofWorkman::FindPointWithResource(ResourceType type, const bool notify) if(!pts.empty()) return pts.front(); - if(notify) - workplace->OnOutOfResources(); - return MapPoint::Invalid(); } diff --git a/libs/s25main/figures/nofWorkman.h b/libs/s25main/figures/nofWorkman.h index eb730a4be5..850b7a3808 100644 --- a/libs/s25main/figures/nofWorkman.h +++ b/libs/s25main/figures/nofWorkman.h @@ -37,7 +37,7 @@ class nofWorkman : public nofBuildingWorker virtual bool StartWorking(); /// Looks for a point with a given resource on the node - MapPoint FindPointWithResource(ResourceType type, bool notify = true) const; + MapPoint FindPointWithResource(ResourceType type) const; public: nofWorkman(Job job, MapPoint pos, unsigned char player, nobUsual* workplace); diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 5a54ad2b23..838baef652 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -26,7 +26,7 @@ AddonId GetMineResourceBehaviorAddonId(const BuildingType buildingType) case BuildingType::GoldMine: return AddonId::GOLDMINE_RESOURCE_BEHAVIOR; case BuildingType::IronMine: return AddonId::IRONMINE_RESOURCE_BEHAVIOR; case BuildingType::CoalMine: return AddonId::COALMINE_RESOURCE_BEHAVIOR; - default: return AddonId::INEXHAUSTIBLE_GRANITEMINES; + default: return AddonId::GRANITEMINE_RESOURCE_BEHAVIOR; } } diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 9279b7d9e5..862e0983c5 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -181,7 +181,7 @@ BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingAIResourceOnly, EmptyWo BOOST_FIXTURE_TEST_CASE(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) { makeWorldMineable(world); - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); makeMineNodesUsableForSearch(ai, world, 0); @@ -194,7 +194,7 @@ BOOST_FIXTURE_TEST_CASE(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixtu BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteDoesNotImplyWorkEverywhereForAI, EmptyWorldFixture1P) { makeWorldMineable(world); - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, 1); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); makeMineNodesUsableForSearch(ai, world, 0); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 53d6777fa2..71b56a64a0 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -188,7 +188,7 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithou BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteMineWithoutResourcesFixture) { - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 1); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, 1); CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; @@ -200,7 +200,7 @@ BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteM BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource, GraniteMineWithoutResourcesFixture) { - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; @@ -213,7 +213,7 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereIgnoresExistingResource, MineProductionFixture) { - ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateMine(BuildingType::GraniteMine, Resource(ResourceType::Coal, 4)); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; From 6e5402912acb4eef37fe1ad345aa1f0ef5382e35 Mon Sep 17 00:00:00 2001 From: Manuel Ries Date: Mon, 20 Jul 2026 11:29:51 +0200 Subject: [PATCH 18/23] Fix mine resource behavior regressions found during verification - tests/testGamePlayer.cpp: move FindMinePosition into the anonymous namespace. Without this the build fails with -Werror=missing-declarations on GCC and Clang. - nofWellguy: report running out of resources explicitly. Moving the notification out of nofWorkman::FindPointWithResource only re-added it for mines, so wells stopped reporting "This well has dried out": no post message, no NoRessources BuildingNote, productivity was not reset and the DEMOLISH_BLD_WO_RES addon no longer triggered. Adds a regression test that fails without the fix. - GlobalGameSettings: apply the legacy INEXHAUSTIBLE_MINES migration to every mine type that is still at its default behavior. GRANITEMINE_RESOURCE_BEHAVIOR reuses the id of the old INEXHAUSTIBLE_GRANITEMINES bool addon, so old settings, savegames and replays always contain a value for that id and granite mines were silently left exhaustible even though the old global setting made them inexhaustible. Adds a regression test. - MaxEnumValue.h: drop the redundant inline on the constexpr isValidEnumValue, which clang-tidy rejects (readability-redundant-inline-specifier). - Apply clang-format 10 to the lines that exceeded the column limit after the addon rename (testAI.cpp, testProduction.cpp). The CI formatting job rejected them. - Remove leftovers: the now single-use CalcAverageProductivity helper taking a member function pointer, the unused nobUsual::GetProductivityPointer() (which would return the unscaled value) and the always-true depletable check in the S4-like production branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- libs/common/include/helpers/MaxEnumValue.h | 2 +- libs/s25main/BuildingRegister.cpp | 26 ++++++---------- libs/s25main/GlobalGameSettings.cpp | 31 +++++-------------- libs/s25main/buildings/nobUsual.h | 2 -- libs/s25main/figures/nofMiner.cpp | 4 +-- libs/s25main/figures/nofWellguy.cpp | 8 ++++- tests/s25Main/integration/testAI.cpp | 3 +- tests/s25Main/integration/testGamePlayer.cpp | 2 ++ tests/s25Main/integration/testProduction.cpp | 27 ++++++++++++++-- .../s25Main/integration/testSerialization.cpp | 22 +++++++++++++ 10 files changed, 79 insertions(+), 48 deletions(-) diff --git a/libs/common/include/helpers/MaxEnumValue.h b/libs/common/include/helpers/MaxEnumValue.h index 4f6a99e33d..8b5aa1f40c 100644 --- a/libs/common/include/helpers/MaxEnumValue.h +++ b/libs/common/include/helpers/MaxEnumValue.h @@ -32,7 +32,7 @@ inline constexpr unsigned NumEnumValues_v = MaxEnumValue_v + 1u; /// Check whether a numeric value is a valid enumerator for an enum type template -inline constexpr bool isValidEnumValue(unsigned value) +constexpr bool isValidEnumValue(unsigned value) { return value <= MaxEnumValue_v; } diff --git a/libs/s25main/BuildingRegister.cpp b/libs/s25main/BuildingRegister.cpp index 8ae1a30a5a..13a5e8094b 100644 --- a/libs/s25main/BuildingRegister.cpp +++ b/libs/s25main/BuildingRegister.cpp @@ -17,21 +17,6 @@ #include "gameData/BuildingConsts.h" #include "gameData/BuildingProperties.h" -namespace { -unsigned CalcAverageProductivity(const std::list& buildings, - unsigned short (nobUsual::*getProductivity)() const) -{ - const unsigned numBlds = buildings.size(); - if(numBlds == 0) - return 0; - - unsigned productivity = 0; - for(const nobUsual* bld : buildings) - productivity += (bld->*getProductivity)(); - return productivity / numBlds; -} -} // namespace - void BuildingRegister::Serialize(SerializedGameData& sgd) const { sgd.PushObjectContainer(warehouses); @@ -174,7 +159,16 @@ unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const { if(holds_alternative(BLD_WORK_DESC[bldType].producedWare)) return 0; - return ::CalcAverageProductivity(GetBuildings(bldType), &nobUsual::GetProductivity); + unsigned productivity = 0; + const auto& buildings = GetBuildings(bldType); + const unsigned numBlds = buildings.size(); + if(numBlds > 0) + { + for(const nobUsual* bld : buildings) + productivity += bld->GetProductivity(); + productivity /= numBlds; + } + return productivity; } unsigned short BuildingRegister::CalcAverageProductivity() const diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index e02cd5875a..950d401276 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -24,22 +24,15 @@ namespace { constexpr std::array MINE_BUILDING_TYPES = {BuildingType::GraniteMine, BuildingType::CoalMine, BuildingType::IronMine, BuildingType::GoldMine}; -helpers::OptionalEnum GetMineBuildingTypeForAddonId(const AddonId id) +/// Apply the legacy global "inexhaustible mines" setting to all mine types that are still at their default behavior. +/// Only mine types with an explicitly configured non-default behavior keep their setting. This is required because +/// GRANITEMINE_RESOURCE_BEHAVIOR reuses the id of the old INEXHAUSTIBLE_GRANITEMINES bool addon, so old data always +/// contains a value for it (usually 0), while the old global setting made granite mines inexhaustible as well. +void MigrateLegacyInexhaustibleMines(GlobalGameSettings& settings) { for(const BuildingType mineType : MINE_BUILDING_TYPES) { - if(GetMineResourceBehaviorAddonId(mineType) == id) - return mineType; - } - return boost::none; -} - -void MigrateLegacyInexhaustibleMines(GlobalGameSettings& settings, - const helpers::EnumArray& hasMineBehaviorSetting) -{ - for(const BuildingType mineType : MINE_BUILDING_TYPES) - { - if(!hasMineBehaviorSetting[mineType]) + if(GetMineResourceBehavior(settings, mineType) == MineResourceBehavior::Default) settings.setSelection(GetMineResourceBehaviorAddonId(mineType), static_cast(MineResourceBehavior::Inexhaustible)); } @@ -220,7 +213,6 @@ void GlobalGameSettings::LoadSettings() resetAddons(); bool migrateLegacyInexhaustibleMines = false; - helpers::EnumArray hasMineBehaviorSetting{}; for(const auto& it : SETTINGS.addons.configuration) { const auto id = static_cast(it.first); @@ -231,13 +223,10 @@ void GlobalGameSettings::LoadSettings() continue; } - if(const auto mineType = GetMineBuildingTypeForAddonId(id)) - hasMineBehaviorSetting[*mineType] = true; - setSelection(id, status); } if(migrateLegacyInexhaustibleMines) - MigrateLegacyInexhaustibleMines(*this, hasMineBehaviorSetting); + MigrateLegacyInexhaustibleMines(*this); } /** @@ -293,7 +282,6 @@ void GlobalGameSettings::Deserialize(Serializer& ser) resetAddons(); bool migrateLegacyInexhaustibleMines = false; - helpers::EnumArray hasMineBehaviorSetting{}; for(unsigned i = 0; i < count; ++i) { auto addon = static_cast(ser.PopUnsignedInt()); @@ -304,13 +292,10 @@ void GlobalGameSettings::Deserialize(Serializer& ser) continue; } - if(const auto mineType = GetMineBuildingTypeForAddonId(addon)) - hasMineBehaviorSetting[*mineType] = true; - setSelection(addon, status); } if(migrateLegacyInexhaustibleMines) - MigrateLegacyInexhaustibleMines(*this, hasMineBehaviorSetting); + MigrateLegacyInexhaustibleMines(*this); } void GlobalGameSettings::setSelection(AddonId id, unsigned selection) diff --git a/libs/s25main/buildings/nobUsual.h b/libs/s25main/buildings/nobUsual.h index a128c09299..adb25ecdc1 100644 --- a/libs/s25main/buildings/nobUsual.h +++ b/libs/s25main/buildings/nobUsual.h @@ -99,8 +99,6 @@ class nobUsual : public noBuilding /// Bestellte Waren bool AreThereAnyOrderedWares() const; - /// Gibt Pointer auf Produktivität zurück - const unsigned short* GetProductivityPointer() const { return &productivity; } unsigned short GetProductivity() const; const nofBuildingWorker* GetWorker() const { return worker; } diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 2b88cd0ebc..2c847b4a73 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -156,8 +156,8 @@ helpers::OptionalEnum nofMiner::ProduceWare() if(produceNothingThisCycle) return GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); - if(IsMineResourceDepletable(settings, workplace->GetBuildingType())) - ReduceS4LikeResource(*world, resourcePts); + // S4-like exhaustion always depletes, but only down to the minimum amount + ReduceS4LikeResource(*world, resourcePts); } switch(workplace->GetBuildingType()) diff --git a/libs/s25main/figures/nofWellguy.cpp b/libs/s25main/figures/nofWellguy.cpp index dc7f600376..af76f1f6bd 100644 --- a/libs/s25main/figures/nofWellguy.cpp +++ b/libs/s25main/figures/nofWellguy.cpp @@ -118,14 +118,20 @@ helpers::OptionalEnum nofWellguy::ProduceWare() bool nofWellguy::AreWaresAvailable() const { // Check for water - return FindPointWithResource(ResourceType::Water).isValid(); + const bool hasWater = FindPointWithResource(ResourceType::Water).isValid(); + if(!hasWater) + workplace->OnOutOfResources(); + return hasWater; } bool nofWellguy::StartWorking() { MapPoint resPt = FindPointWithResource(ResourceType::Water); if(!resPt.isValid()) + { + workplace->OnOutOfResources(); return false; + } if(world->GetGGS().getSelection(AddonId::EXHAUSTIBLE_WATER) == 2) world->ReduceResource(resPt); return nofWorkman::StartWorking(); diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 862e0983c5..e999aafd03 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -181,7 +181,8 @@ BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingAIResourceOnly, EmptyWo BOOST_FIXTURE_TEST_CASE(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) { makeWorldMineable(world); - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::WorkEverywhere)); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); makeMineNodesUsableForSearch(ai, world, 0); diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 6416de6191..1e963886d6 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -60,6 +60,7 @@ RTTR_ATTRIBUTE_NO_UBSAN(vptr) void setProductivity(nobUsual* bld, unsigned short using WorldFixtureEmpty1P = WorldFixture + 14, 4>; using WorldFixtureMineRadius1P = WorldFixture; +namespace { MapPoint FindMinePosition(const WorldFixtureMineRadius1P& fixture) { for(MapCoord y = MINER_RADIUS; y + MINER_RADIUS < fixture.world.GetSize().y; ++y) @@ -74,6 +75,7 @@ MapPoint FindMinePosition(const WorldFixtureMineRadius1P& fixture) return MapPoint::Invalid(); } +} // namespace BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) { diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 71b56a64a0..fa117dddd5 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -200,7 +200,8 @@ BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteM BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource, GraniteMineWithoutResourcesFixture) { - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateGraniteMineWithoutResources(); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; @@ -213,7 +214,8 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereIgnoresExistingResource, MineProductionFixture) { - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::WorkEverywhere)); const MapPoint minePos = CreateMine(BuildingType::GraniteMine, Resource(ResourceType::Coal, 4)); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; @@ -375,4 +377,25 @@ BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorProducesWithoutCreatingRes BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); } +// Regression test: the out-of-resources notification was moved from nofWorkman::FindPointWithResource into its +// callers, so every caller (not only mines) must still report it. +BOOST_FIXTURE_TEST_CASE(WellWithoutWaterReportsOutOfResources, WorldWithGCExecution1P) +{ + PostBox& postbox = world.GetPostMgr().AddPostBox(curPlayer); + GoodsAndPeopleCounts inv; + inv[GoodType::Fish] = 40; + inv[Job::Helper] = 2; + world.GetSpecObj(hqPos)->AddToInventory(inv, true); + + const MapPoint wellPos = hqPos + MapPoint(2, 0); + const auto* well = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::Well, wellPos, curPlayer, Nation::Romans)); + BuildRoad(world.GetNeighbour(wellPos, Direction::SouthEast), false, std::vector(2, Direction::West)); + RTTR_EXEC_TILL(500, well->HasWorker()); + + // The empty test world has no water resources at all, so the well must report that it dried out + RTTR_EXEC_TILL(2000, postbox.GetNumMsgs() > 0u); + BOOST_TEST(well->GetProductivity() == 0u); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/s25Main/integration/testSerialization.cpp b/tests/s25Main/integration/testSerialization.cpp index 06844a2609..0a7c966b11 100644 --- a/tests/s25Main/integration/testSerialization.cpp +++ b/tests/s25Main/integration/testSerialization.cpp @@ -234,6 +234,28 @@ BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeMigratesToPerMineBehavio BOOST_TEST(ggsLoaded.getSelection(AddonId::INEXHAUSTIBLE_MINES) == 0u); } +BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeAlsoMigratesLegacyGraniteAddonValue) +{ + // Old data always contains a value for 0x00800000 (formerly INEXHAUSTIBLE_GRANITEMINES, now + // GRANITEMINE_RESOURCE_BEHAVIOR). A disabled granite addon must not prevent the legacy global setting from + // making granite mines inexhaustible, which is what the old code did. + Serializer ser; + PushSerializedGGSHeader(ser); + ser.PushUnsignedInt(2); + ser.PushUnsignedInt(static_cast(AddonId::INEXHAUSTIBLE_MINES)); + ser.PushUnsignedInt(1); + ser.PushUnsignedInt(static_cast(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR)); + ser.PushUnsignedInt(0); + + Serializer loader(ser.GetData(), ser.GetLength()); + GlobalGameSettings ggsLoaded; + ggsLoaded.Deserialize(loader); + + for(const BuildingType mineType : + {BuildingType::GraniteMine, BuildingType::CoalMine, BuildingType::IronMine, BuildingType::GoldMine}) + BOOST_TEST(GetMineResourceBehavior(ggsLoaded, mineType) == MineResourceBehavior::Inexhaustible); +} + BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeDoesNotOverridePerMineBehavior) { Serializer ser; From 773a9c6cd9fd0905a0f867e493b79242595f4e7f Mon Sep 17 00:00:00 2001 From: Manuel Ries Date: Mon, 20 Jul 2026 13:20:29 +0200 Subject: [PATCH 19/23] Use empty OptionalEnum instead of boost::none Upstream master replaced boost::optional by std::optional, so helpers::OptionalEnum no longer accepts boost::none. Returning {} works both before and after that change. Co-Authored-By: Claude Opus 4.8 (1M context) --- libs/s25main/ai/AIResource.h | 2 +- libs/s25main/figures/nofMiner.cpp | 6 +++--- libs/s25main/gameTypes/MineResourceBehavior.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/s25main/ai/AIResource.h b/libs/s25main/ai/AIResource.h index 00f01bfca0..7defefd42f 100644 --- a/libs/s25main/ai/AIResource.h +++ b/libs/s25main/ai/AIResource.h @@ -97,7 +97,7 @@ inline helpers::OptionalEnum convertToResourceType(AIResource res) case AIResource::Coal: return ResourceType::Coal; case AIResource::Granite: return ResourceType::Granite; case AIResource::Fish: return ResourceType::Fish; - default: return boost::none; + default: return {}; } } diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 2c847b4a73..1393d90437 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -51,7 +51,7 @@ helpers::OptionalEnum GetLowerGradeFallbackGood(const BuildingType bui case BuildingType::GoldMine: return GoodType::IronOre; case BuildingType::IronMine: return GoodType::Coal; case BuildingType::CoalMine: return GoodType::Stones; - default: return boost::none; + default: return {}; } } @@ -67,13 +67,13 @@ helpers::OptionalEnum GetNoOutputFallbackGood(const GlobalGameSettings < graniteFallbackChance) return GoodType::Stones; - return boost::none; + return {}; } if(fallback == MineNoOutputFallback::ProduceLowerGradeResource) return GetLowerGradeFallbackGood(buildingType); - return boost::none; + return {}; } std::vector GetPointsWithResource(const GameWorld& world, const MapPoint pos, const ResourceType type) diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 838baef652..8c31a7ce88 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -49,7 +49,7 @@ helpers::OptionalEnum GetMineBuildingType(const ResourceType resou case ResourceType::Iron: return BuildingType::IronMine; case ResourceType::Coal: return BuildingType::CoalMine; case ResourceType::Granite: return BuildingType::GraniteMine; - default: return boost::none; + default: return {}; } } From 9305cea8a8c29896a27e2cf1c722ded6139797d3 Mon Sep 17 00:00:00 2001 From: Manuel Ries Date: Mon, 20 Jul 2026 13:20:30 +0200 Subject: [PATCH 20/23] Keep the legacy mine addon names usable from Lua Renaming INEXHAUSTIBLE_GRANITEMINES and dropping the global INEXHAUSTIBLE_MINES addon also changed the ADDON_* constants exported to Lua, which doc/lua/functions.md documents as public API. Existing map scripts using the old names would have silently stopped working. Add an explicit alias for the renamed addon (same id, same meaning of value 1) and route the removed global addon through the same migration that is used for old settings and savegames. The obsolete addon stays unregistered, so neither the addon UI nor serialization change. Co-Authored-By: Claude Opus 4.8 (1M context) --- doc/lua/functions.md | 5 +++ libs/s25main/GlobalGameSettings.cpp | 31 +++++++++--------- libs/s25main/GlobalGameSettings.h | 3 ++ libs/s25main/lua/LuaInterfaceSettings.cpp | 12 ++++++- tests/s25Main/lua/testLuaSettings.cpp | 39 +++++++++++++++++++++++ 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/doc/lua/functions.md b/doc/lua/functions.md index b49cd978d0..927dab53d1 100644 --- a/doc/lua/functions.md +++ b/doc/lua/functions.md @@ -113,6 +113,11 @@ rttr:SetAddon(ADDON_FRONTIER_DISTANCE_REACHABLE, true) For addons which can be enabled / disabled simply use true or false, for addons with different settings, add the number according to your setting. +Deprecated addon names, kept working for older scripts: + +- `ADDON_INEXHAUSTIBLE_GRANITEMINES` is an alias for `ADDON_GRANITEMINE_RESOURCE_BEHAVIOR`. The old `true` still selects the inexhaustible behavior. +- `ADDON_INEXHAUSTIBLE_MINES` is no longer an addon of its own. Setting it to `true` makes every mine type that is still at its default behavior inexhaustible, setting it to `false` does nothing. Use the per-mine `ADDON_*MINE_RESOURCE_BEHAVIOR` addons instead. + **rttr:ResetAddons()** Set all addons to S2 defaults diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 950d401276..ac0c65a02d 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -23,20 +23,6 @@ namespace { constexpr std::array MINE_BUILDING_TYPES = {BuildingType::GraniteMine, BuildingType::CoalMine, BuildingType::IronMine, BuildingType::GoldMine}; - -/// Apply the legacy global "inexhaustible mines" setting to all mine types that are still at their default behavior. -/// Only mine types with an explicitly configured non-default behavior keep their setting. This is required because -/// GRANITEMINE_RESOURCE_BEHAVIOR reuses the id of the old INEXHAUSTIBLE_GRANITEMINES bool addon, so old data always -/// contains a value for it (usually 0), while the old global setting made granite mines inexhaustible as well. -void MigrateLegacyInexhaustibleMines(GlobalGameSettings& settings) -{ - for(const BuildingType mineType : MINE_BUILDING_TYPES) - { - if(GetMineResourceBehavior(settings, mineType) == MineResourceBehavior::Default) - settings.setSelection(GetMineResourceBehaviorAddonId(mineType), - static_cast(MineResourceBehavior::Inexhaustible)); - } -} } // namespace GlobalGameSettings::GlobalGameSettings() @@ -226,7 +212,7 @@ void GlobalGameSettings::LoadSettings() setSelection(id, status); } if(migrateLegacyInexhaustibleMines) - MigrateLegacyInexhaustibleMines(*this); + applyLegacyInexhaustibleMines(); } /** @@ -295,7 +281,20 @@ void GlobalGameSettings::Deserialize(Serializer& ser) setSelection(addon, status); } if(migrateLegacyInexhaustibleMines) - MigrateLegacyInexhaustibleMines(*this); + applyLegacyInexhaustibleMines(); +} + +/// Only mine types with an explicitly configured non-default behavior keep their setting. This is required because +/// GRANITEMINE_RESOURCE_BEHAVIOR reuses the id of the old INEXHAUSTIBLE_GRANITEMINES bool addon, so old data always +/// contains a value for it (usually 0), while the old global setting made granite mines inexhaustible as well. +void GlobalGameSettings::applyLegacyInexhaustibleMines() +{ + for(const BuildingType mineType : MINE_BUILDING_TYPES) + { + if(GetMineResourceBehavior(*this, mineType) == MineResourceBehavior::Default) + setSelection(GetMineResourceBehaviorAddonId(mineType), + static_cast(MineResourceBehavior::Inexhaustible)); + } } void GlobalGameSettings::setSelection(AddonId id, unsigned selection) diff --git a/libs/s25main/GlobalGameSettings.h b/libs/s25main/GlobalGameSettings.h index f37d52ab12..111c0c04df 100644 --- a/libs/s25main/GlobalGameSettings.h +++ b/libs/s25main/GlobalGameSettings.h @@ -47,6 +47,9 @@ class GlobalGameSettings bool isEnabled(AddonId id) const; unsigned getSelection(AddonId id) const; void setSelection(AddonId id, unsigned selection); + /// Apply the removed global INEXHAUSTIBLE_MINES addon: make every mine type that is still at its default behavior + /// inexhaustible. Used to migrate old settings, savegames and Lua scripts. + void applyLegacyInexhaustibleMines(); /// loads the saved addon configuration from the SETTINGS. void LoadSettings(); diff --git a/libs/s25main/lua/LuaInterfaceSettings.cpp b/libs/s25main/lua/LuaInterfaceSettings.cpp index 8f29d5658f..73bed6fc73 100644 --- a/libs/s25main/lua/LuaInterfaceSettings.cpp +++ b/libs/s25main/lua/LuaInterfaceSettings.cpp @@ -51,6 +51,9 @@ void LuaInterfaceSettings::Register(kaguya::State& state) { state[std::string("ADDON_") + rttrEnum::toString(id)] = AddonIdWrapper{id}; } + // Legacy alias: the bool addon was replaced by a list addon reusing the same id, where the old value 1 still means + // "inexhaustible". Keeps existing map scripts working; may be removed once INEXHAUSTIBLE_MINES is dropped too. + state["ADDON_INEXHAUSTIBLE_GRANITEMINES"] = AddonIdWrapper{AddonId::GRANITEMINE_RESOURCE_BEHAVIOR}; #pragma region ConstDefs #define ADD_LUA_CONST(name) state["GS_" + s25util::toUpper(#name)] = GameSpeed::name @@ -98,7 +101,14 @@ LuaServerPlayer LuaInterfaceSettings::GetPlayer(int idx) void LuaInterfaceSettings::SetAddon(AddonIdWrapper id, unsigned value) { GlobalGameSettings ggs = lobbyServerController_.GetGGS(); - ggs.setSelection(id, value); + // The global "inexhaustible mines" addon was replaced by per-mine-type settings and is no longer registered. + // Apply the same migration used for old settings and savegames instead of reporting an unknown addon. + if(static_cast(id) == AddonId::INEXHAUSTIBLE_MINES) + { + if(value != 0) + ggs.applyLegacyInexhaustibleMines(); + } else + ggs.setSelection(id, value); lobbyServerController_.ChangeGlobalGameSettings(ggs); } diff --git a/tests/s25Main/lua/testLuaSettings.cpp b/tests/s25Main/lua/testLuaSettings.cpp index 31be92dc24..29917b8786 100644 --- a/tests/s25Main/lua/testLuaSettings.cpp +++ b/tests/s25Main/lua/testLuaSettings.cpp @@ -10,7 +10,9 @@ #include "lua/LuaInterfaceSettings.h" #include "network/IGameLobbyController.h" #include "worldFixtures/MockLocalGameState.h" +#include "gameTypes/BuildingType.h" #include "gameTypes/GameTypesOutput.h" +#include "gameTypes/MineResourceBehavior.h" #include "s25util/colors.h" #include #include @@ -265,6 +267,43 @@ BOOST_AUTO_TEST_CASE(SettingsFunctions) } } +BOOST_AUTO_TEST_CASE(LegacyMineAddons) +{ + LogAccessor logAcc; + const auto behaviorOf = [this](BuildingType bld) { return ggs.getSelection(GetMineResourceBehaviorAddonId(bld)); }; + constexpr auto defaultBehavior = static_cast(MineResourceBehavior::Default); + constexpr auto inexhaustible = static_cast(MineResourceBehavior::Inexhaustible); + constexpr auto s4Like = static_cast(MineResourceBehavior::S4LikeExhaustion); + constexpr BuildingType allMines[] = {BuildingType::GraniteMine, BuildingType::CoalMine, BuildingType::IronMine, + BuildingType::GoldMine}; + + // Renamed addon: the old Lua name must still resolve to the same addon and value 1 must still mean inexhaustible + executeLua("rttr:SetAddon(ADDON_INEXHAUSTIBLE_GRANITEMINES, true)"); + BOOST_TEST_REQUIRE(behaviorOf(BuildingType::GraniteMine) == inexhaustible); + BOOST_TEST_REQUIRE(behaviorOf(BuildingType::CoalMine) == defaultBehavior); + + // Removed global addon: scripts using it must still make all mines inexhaustible + executeLua("rttr:ResetAddons()"); + executeLua("rttr:SetAddon(ADDON_INEXHAUSTIBLE_MINES, true)"); + for(const BuildingType bld : allMines) + BOOST_TEST_REQUIRE(behaviorOf(bld) == inexhaustible); + + // ... but mine types configured explicitly beforehand keep their setting + executeLua("rttr:ResetAddons()"); + executeLua("rttr:SetAddon(ADDON_GOLDMINE_RESOURCE_BEHAVIOR, 2)"); + executeLua("rttr:SetAddon(ADDON_INEXHAUSTIBLE_MINES, true)"); + BOOST_TEST_REQUIRE(behaviorOf(BuildingType::GoldMine) == s4Like); + BOOST_TEST_REQUIRE(behaviorOf(BuildingType::CoalMine) == inexhaustible); + + // Disabling the legacy addon changes nothing + executeLua("rttr:ResetAddons()"); + executeLua("rttr:SetAddon(ADDON_INEXHAUSTIBLE_MINES, false)"); + for(const BuildingType bld : allMines) + BOOST_TEST_REQUIRE(behaviorOf(bld) == defaultBehavior); + // The legacy addon must not be registered, so it cannot show up twice in the addon UI + BOOST_TEST_REQUIRE(ggs.getSelection(AddonId::INEXHAUSTIBLE_MINES) == 0u); +} + BOOST_AUTO_TEST_CASE(PlayerSettings) { LogAccessor logAcc; From 19acd114a27bc541163fa30e939fa35eb2878eb4 Mon Sep 17 00:00:00 2001 From: mries Date: Thu, 30 Jul 2026 12:57:33 +0200 Subject: [PATCH 21/23] Address Flamefire review: self-documenting tests, GetFeatureLevel bump to 7, cleanup magic numbers and test structure Co-Authored-By: Claude --- libs/s25main/figures/nofMiner.cpp | 8 +- .../gameTypes/MineResourceBehavior.cpp | 12 + libs/s25main/gameTypes/MineResourceBehavior.h | 3 + libs/s25main/lua/LuaInterfaceGameBase.cpp | 4 +- tests/s25Main/integration/testAI.cpp | 68 ++--- tests/s25Main/integration/testGamePlayer.cpp | 122 ++++----- tests/s25Main/integration/testProduction.cpp | 246 ++++++++---------- .../s25Main/integration/testSerialization.cpp | 7 +- 8 files changed, 233 insertions(+), 237 deletions(-) diff --git a/libs/s25main/figures/nofMiner.cpp b/libs/s25main/figures/nofMiner.cpp index 1393d90437..5f5da8e384 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -160,13 +160,7 @@ helpers::OptionalEnum nofMiner::ProduceWare() ReduceS4LikeResource(*world, resourcePts); } - switch(workplace->GetBuildingType()) - { - case BuildingType::GoldMine: return GoodType::Gold; - case BuildingType::IronMine: return GoodType::IronOre; - case BuildingType::CoalMine: return GoodType::Coal; - default: return GoodType::Stones; - } + return GetMineOutput(workplace->GetBuildingType()); } bool nofMiner::AreWaresAvailable() const diff --git a/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp index 8c31a7ce88..91ad66f940 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.cpp +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -7,6 +7,7 @@ #include "addons/const_addons.h" #include "world/GameWorld.h" #include "gameTypes/BuildingType.h" +#include "gameTypes/GoodTypes.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/Resource.h" #include "gameData/GameConsts.h" @@ -41,6 +42,17 @@ ResourceType GetMineResourceType(const BuildingType buildingType) } } +GoodType GetMineOutput(const BuildingType buildingType) +{ + switch(buildingType) + { + case BuildingType::GoldMine: return GoodType::Gold; + case BuildingType::IronMine: return GoodType::IronOre; + case BuildingType::CoalMine: return GoodType::Coal; + default: return GoodType::Stones; + } +} + helpers::OptionalEnum GetMineBuildingType(const ResourceType resourceType) { switch(resourceType) diff --git a/libs/s25main/gameTypes/MineResourceBehavior.h b/libs/s25main/gameTypes/MineResourceBehavior.h index 5ca06f56a3..cf2193664a 100644 --- a/libs/s25main/gameTypes/MineResourceBehavior.h +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -13,6 +13,7 @@ class GameWorld; enum class AddonId; enum class BuildingType : unsigned char; enum class ResourceType : uint8_t; +enum class GoodType : uint8_t; enum class MineResourceBehavior { @@ -28,6 +29,8 @@ constexpr auto maxEnumValue(MineResourceBehavior) AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); ResourceType GetMineResourceType(BuildingType buildingType); +/// The ware a mine of the given type produces (granite/stones for any non-mineral mine) +GoodType GetMineOutput(BuildingType buildingType); helpers::OptionalEnum GetMineBuildingType(ResourceType resourceType); unsigned GetRemainingMineResources(const GameWorld& world, MapPoint pos, ResourceType resourceType); unsigned GetS4LikeMineProductionChance(unsigned remainingMatchingResources); diff --git a/libs/s25main/lua/LuaInterfaceGameBase.cpp b/libs/s25main/lua/LuaInterfaceGameBase.cpp index 7f6e4f49ae..2b369b6d9b 100644 --- a/libs/s25main/lua/LuaInterfaceGameBase.cpp +++ b/libs/s25main/lua/LuaInterfaceGameBase.cpp @@ -16,7 +16,9 @@ unsigned LuaInterfaceGameBase::GetVersion() unsigned LuaInterfaceGameBase::GetFeatureLevel() { - return 6; + // 7: Added the per-mine ADDON_*MINE_RESOURCE_BEHAVIOR addons with the legacy ADDON_INEXHAUSTIBLE_GRANITEMINES / + // ADDON_INEXHAUSTIBLE_MINES aliases, plus SetAI resetting the player name (see doc/lua/functions.md). + return 7; } LuaInterfaceGameBase::LuaInterfaceGameBase(const ILocalGameState& localGameState) : localGameState(localGameState) diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index e999aafd03..c69296d987 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -74,7 +74,9 @@ void makeWorldMineable(GameWorld& world) world.InitAfterLoad(); } -void makeMineNodesUsableForSearch(AIJH::AIPlayerJH& aijh, const GameWorld& world, const unsigned player) +// Mark every node as a usable, owned, reachable build spot so the AI's position search only depends on the +// mine resource behavior under test and not on unrelated terrain/ownership state. +void initAIJhNodes(AIJH::AIPlayerJH& aijh, const GameWorld& world, const unsigned player) { RTTR_FOREACH_PT(MapPoint, world.GetSize()) { @@ -150,58 +152,64 @@ BOOST_FIXTURE_TEST_CASE(MineResourceRatingAccountsForS4LikeExhaustion, EmptyWorl world.GetNodeWriteable(resourcePos).resources = Resource(ResourceType::Coal, 1); MockAI ai(0, world, AI::Level::Easy); - const int defaultRating = ai.getAIInterface().GetResourceRating(resourcePos, AIResource::Coal); - - ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, - static_cast(MineResourceBehavior::S4LikeExhaustion)); - const int s4LikeRating = ai.getAIInterface().GetResourceRating(resourcePos, AIResource::Coal); + const auto ratingWith = [&](const MineResourceBehavior behavior) { + ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(behavior)); + return ai.getAIInterface().GetResourceRating(resourcePos, AIResource::Coal); + }; + // A spot with a coal deposit gets the full radius rating for all behaviors that mine the deposit as-is... + const int defaultRating = ratingWith(MineResourceBehavior::Default); BOOST_TEST(defaultRating == static_cast(RES_RADIUS[AIResource::Coal])); + BOOST_TEST(ratingWith(MineResourceBehavior::Inexhaustible) == defaultRating); + BOOST_TEST(ratingWith(MineResourceBehavior::WorkEverywhere) == defaultRating); + + // ...only S4-like exhaustion lowers the rating because a nearly depleted deposit produces less over its lifetime. + const int s4LikeRating = ratingWith(MineResourceBehavior::S4LikeExhaustion); BOOST_TEST(s4LikeRating > 0); BOOST_TEST(s4LikeRating < defaultRating); } -BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingAIResourceOnly, EmptyWorldFixture1P) +BOOST_FIXTURE_TEST_CASE(MineWorkEverywhereAffectsMatchingResourceOnly, EmptyWorldFixture1P) { + // Whole world is mineable terrain but has no mineral deposits at all. makeWorldMineable(world); ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); - makeMineNodesUsableForSearch(ai, world, 0); + initAIJhNodes(ai, world, 0); - const MapPoint around = world.GetPlayer(0).GetHQPos(); - const MapPoint otherResourcePos = world.MakeMapPoint(around + Position(8, 0)); - world.GetNodeWriteable(otherResourcePos).resources = Resource(ResourceType::Iron, 4); - BOOST_TEST(ai.getAIInterface().GetResourceRating(otherResourcePos, AIResource::Coal) + // The HQ is the only guaranteed reachable/owned starting spot, so searches are centered on it. + const MapPoint searchCenter = world.GetPlayer(0).GetHQPos(); + // The only iron deposit sits well outside the search radius below, so default iron mining has no reachable spot. + const MapPoint ironPos = world.MakeMapPoint(searchCenter + Position(8, 0)); + world.GetNodeWriteable(ironPos).resources = Resource(ResourceType::Iron, 4); + + // WorkEverywhere coal treats any mineable node as a coal spot, even one that actually holds an iron deposit. + BOOST_TEST(ai.getAIInterface().GetResourceRating(ironPos, AIResource::Coal) == static_cast(RES_RADIUS[AIResource::Coal])); - BOOST_TEST(ai.FindBestPosition(around, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); - BOOST_TEST(!ai.FindBestPosition(around, AIResource::Ironore, BuildingQuality::Mine, 5).isValid()); + // So coal finds a spot right around the HQ, while default iron finds none because its only deposit is out of range. + BOOST_TEST(ai.FindBestPosition(searchCenter, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); + BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Ironore, BuildingQuality::Mine, 5).isValid()); } -BOOST_FIXTURE_TEST_CASE(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) +BOOST_FIXTURE_TEST_CASE(GraniteMineResourceBehaviorAffectsAIMineSearch, EmptyWorldFixture1P) { + // Mineable terrain everywhere but no deposits, so only the granite behavior decides what the AI can place. makeWorldMineable(world); ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); - makeMineNodesUsableForSearch(ai, world, 0); - - const MapPoint around = world.GetPlayer(0).GetHQPos(); - BOOST_TEST(ai.FindBestPosition(around, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); - BOOST_TEST(!ai.FindBestPosition(around, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); -} + initAIJhNodes(ai, world, 0); + const MapPoint searchCenter = world.GetPlayer(0).GetHQPos(); -BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteDoesNotImplyWorkEverywhereForAI, EmptyWorldFixture1P) -{ - makeWorldMineable(world); - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, 1); - - AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); - makeMineNodesUsableForSearch(ai, world, 0); + // WorkEverywhere lets the AI place a granite mine on any mineable node, but only for granite, not coal. + BOOST_TEST(ai.FindBestPosition(searchCenter, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); + BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); - const MapPoint around = world.GetPlayer(0).GetHQPos(); - BOOST_TEST(!ai.FindBestPosition(around, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); + // Switching to Inexhaustible does NOT imply "work everywhere": without an actual deposit the AI finds no spot. + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); + BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); } BOOST_FIXTURE_TEST_CASE(KeepBQUpdated, BiggerWorldWithGCExecution) diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 1e963886d6..7b1c9b2d77 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -61,20 +61,50 @@ using WorldFixtureEmpty1P = WorldFixture; namespace { -MapPoint FindMinePosition(const WorldFixtureMineRadius1P& fixture) +// S4-like mine productivity reaches the mine's base productivity once this many matching resources remain in the +// mine radius, degrading linearly below it (see GetS4LikeMineProductionChance). +constexpr unsigned S4LIKE_FULL_PRODUCTIVITY_AMOUNT = 20; + +// Places a coal mine and drives its S4-like productivity purely through the resources in its radius. +// The 20x12 map is larger than 2*MINER_RADIUS in each dimension, so the mine radius never wraps onto itself. +struct MineProductivityFixture : WorldFixtureMineRadius1P { - for(MapCoord y = MINER_RADIUS; y + MINER_RADIUS < fixture.world.GetSize().y; ++y) + nobUsual* coalMine; + MapPoint minePos; + + MineProductivityFixture() + { + // BuildingFactory::CreateBuilding ignores the building quality, so any node works; offset from the HQ keeps + // the mine radius clear of the HQ. The empty world has no resources, but clear the radius to be explicit. + minePos = world.MakeMapPoint(world.GetPlayer(0).GetHQPos() + Position(4, 0)); + coalMine = static_cast( + BuildingFactory::CreateBuilding(world, BuildingType::CoalMine, minePos, 0, Nation::Romans)); + for(const MapPoint pt : world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS)) + world.SetResource(pt, Resource()); + } + + // Puts coal on the mine node and its eastern neighbor (0 == none), leaving the rest of the radius empty. + void setCoalAmounts(const unsigned atMine, const unsigned atNeighbor) { - for(MapCoord x = MINER_RADIUS; x + MINER_RADIUS < fixture.world.GetSize().x; ++x) + world.SetResource(minePos, atMine ? Resource(ResourceType::Coal, atMine) : Resource()); + world.SetResource(world.GetNeighbour(minePos, Direction::East), + atNeighbor ? Resource(ResourceType::Coal, atNeighbor) : Resource()); + } + + // Spreads the given total coal amount as evenly as possible over every node in the radius (rest set to none). + // Only the summed amount in range matters, so the exact distribution is irrelevant. + void spreadCoalInRadius(const unsigned total) + { + const std::vector pts = world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS); + unsigned remaining = total; + for(unsigned i = 0; i < pts.size(); ++i) { - const MapPoint pt(x, y); - if(fixture.world.GetNode(pt).bq == BuildingQuality::Castle) - return pt; + const unsigned here = remaining / (static_cast(pts.size()) - i); + world.SetResource(pts[i], here ? Resource(ResourceType::Coal, here) : Resource()); + remaining -= here; } } - - return MapPoint::Invalid(); -} +}; } // namespace BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) @@ -143,88 +173,60 @@ BOOST_FIXTURE_TEST_CASE(ProductivityStats, WorldFixtureEmpty1P) BOOST_TEST(buildingRegister.CalcAverageProductivity() == avgProd); } -BOOST_FIXTURE_TEST_CASE(MineProductivityAccountsForS4LikeResourceChance, WorldFixtureEmpty1P) +BOOST_FIXTURE_TEST_CASE(MineProductivityAccountsForS4LikeResourceChance, MineProductivityFixture) { - MapPoint minePos(0, 0); - while(world.GetNode(minePos).bq != BuildingQuality::Castle) - BOOST_TEST_REQUIRE((++minePos.x) < world.GetSize().x); - - auto* coalMine = static_cast( - BuildingFactory::CreateBuilding(world, BuildingType::CoalMine, minePos, 0, Nation::Romans)); setProductivity(coalMine, 100); - world.SetResource(minePos, Resource(ResourceType::Coal, 1)); + // Without the S4-like behavior the base productivity is reported unchanged, regardless of the resources left. + setCoalAmounts(1, 0); BOOST_TEST(coalMine->GetProductivity() == 100u); ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::S4LikeExhaustion)); - BOOST_TEST(coalMine->GetProductivity() == 5u); - world.SetResource(minePos, Resource(ResourceType::Coal, 15)); - world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource(ResourceType::Coal, 5)); + // S4-like scales productivity with the resources left, reaching the full base value at 20 (full productivity). + setCoalAmounts(15, 5); // == S4LIKE_FULL_PRODUCTIVITY_AMOUNT BOOST_TEST(coalMine->GetProductivity() == 100u); - + // Halving the resources halves the reported productivity. + setCoalAmounts(5, 5); + BOOST_TEST(coalMine->GetProductivity() == 50u); + // Lowering the base productivity scales the result by the same factor: 80% of the 50% chance -> 40%. setProductivity(coalMine, 80); - world.SetResource(minePos, Resource(ResourceType::Coal, 10)); - world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource()); BOOST_TEST(coalMine->GetProductivity() == 40u); BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 40u); - + // No resources left -> no production. setProductivity(coalMine, 100); - world.SetResource(minePos, Resource(ResourceType::Coal, 15)); - world.SetResource(world.GetNeighbour(minePos, Direction::East), Resource()); - BOOST_TEST(coalMine->GetProductivity() == 75u); - - world.SetResource(minePos, Resource()); + setCoalAmounts(0, 0); BOOST_TEST(coalMine->GetProductivity() == 0u); + // Inexhaustible mines always report their base productivity again, ignoring the resources. ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); BOOST_TEST(coalMine->GetProductivity() == 100u); } -BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius, WorldFixtureMineRadius1P) +BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius, MineProductivityFixture) { - const MapPoint minePos = FindMinePosition(*this); - BOOST_TEST_REQUIRE(minePos.isValid()); - - auto* coalMine = static_cast( - BuildingFactory::CreateBuilding(world, BuildingType::CoalMine, minePos, 0, Nation::Romans)); + // Base productivity 100 so GetProductivity() directly mirrors the resource-based production chance. setProductivity(coalMine, 100); ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::S4LikeExhaustion)); + // A different resource type in range and matching coal just outside the radius must not count as coal. + world.SetResource(world.GetNeighbour(minePos, Direction::NorthWest), Resource(ResourceType::Iron, 15)); + const MapPoint outOfRangePt = world.GetNeighbour( + world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); const auto inRangePts = world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS); - for(const MapPoint pt : inRangePts) - world.SetResource(pt, Resource()); - + BOOST_TEST_REQUIRE(std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end()); + world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 0u); BOOST_TEST(coalMine->GetProductivity() == 0u); - const MapPoint westPt = world.GetNeighbour(minePos, Direction::West); - const MapPoint eastPt = world.GetNeighbour(minePos, Direction::East); - world.SetResource(westPt, Resource(ResourceType::Coal, 4)); - world.SetResource(eastPt, Resource(ResourceType::Coal, 6)); + // Half of the full amount, but spread across every node of the radius: productivity depends only on the sum in + // range, not on how it is distributed. This overwrites the in-range iron, which no longer matters here. + spreadCoalInRadius(S4LIKE_FULL_PRODUCTIVITY_AMOUNT / 2); BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); BOOST_TEST(coalMine->GetProductivity() == 50u); BOOST_TEST(world.GetPlayer(0).GetBuildingRegister().CalcProductivities()[BuildingType::CoalMine] == 50u); - - world.SetResource(world.GetNeighbour(minePos, Direction::NorthWest), Resource(ResourceType::Iron, 15)); - BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); - BOOST_TEST(coalMine->GetProductivity() == 50u); - - const MapPoint outOfRangePt = world.GetNeighbour( - world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); - const bool isOutOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); - BOOST_TEST_REQUIRE(isOutOfRange); - world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); - BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 10u); - BOOST_TEST(coalMine->GetProductivity() == 50u); - - setProductivity(coalMine, 99); - world.SetResource(westPt, Resource(ResourceType::Coal, 5)); - world.SetResource(eastPt, Resource(ResourceType::Coal, 6)); - BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 11u); - BOOST_TEST(coalMine->GetProductivity() == 54u); } BOOST_FIXTURE_TEST_CASE(IsHQTent_ReturnsFalse_IfPrimaryHQIsNotTent, WorldFixtureEmpty1P) diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index fa117dddd5..e2205487c4 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -13,9 +13,14 @@ #include "gameTypes/MineResourceBehavior.h" #include "gameData/ToolConsts.h" #include +#include +#include #include #include #include +#include + +namespace dataset = boost::unit_test::data; // LCOV_EXCL_START static std::ostream& operator<<(std::ostream& os, const PostCategory& cat) @@ -27,39 +32,20 @@ static std::ostream& operator<<(std::ostream& os, const PostCategory& cat) BOOST_AUTO_TEST_SUITE(Production) namespace { -GoodType GetMineGoodType(const BuildingType mineType) -{ - switch(mineType) - { - case BuildingType::GoldMine: return GoodType::Gold; - case BuildingType::IronMine: return GoodType::IronOre; - case BuildingType::CoalMine: return GoodType::Coal; - default: return GoodType::Stones; - } -} - -struct GraniteMineWithoutResourcesFixture : WorldWithGCExecution1P -{ - MapPoint CreateGraniteMineWithoutResources() - { - GoodsAndPeopleCounts inv; - inv[GoodType::Fish] = 40; - inv[GoodType::PickAxe] = 1; - inv[Job::Miner] = 1; - world.GetSpecObj(hqPos)->AddToInventory(inv, true); - - MapPoint minePos = hqPos + MapPoint(2, 0); - const auto* mine = static_cast( - BuildingFactory::CreateBuilding(world, BuildingType::GraniteMine, minePos, curPlayer, Nation::Romans)); - BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, std::vector(2, Direction::West)); - RTTR_EXEC_TILL(500, mine->HasWorker()); - return minePos; - } -}; - +// Provides a single, connected, staffed mine so tests only differ in the mine type, its resource spot and the +// addon settings under test. The miner supplies are added in the ctor (comment: setup belongs in the fixture); +// the mine itself is created per test because its type/resource is what varies. struct MineProductionFixture : WorldWithGCExecution1P { - void AddMinerSupplies() + // Enough GFs for several miner production cycles. Reused so timing-based tests stay comparable. + static constexpr unsigned maxProductionGFs = 5000; + // Seed + window shared by the two S4-like exhaustion tests so they form a direct comparison: fed the SAME random + // sequence, a near-exhausted mine (1 resource -> ~5% chance) produces nothing within the window, while a full + // mine (many resources -> high chance) reliably completes a depleting cycle. Only the resource amount differs. + static constexpr unsigned s4LikeComparisonSeed = 2; + static constexpr unsigned s4LikeComparisonGFs = 2000; + + MineProductionFixture() { GoodsAndPeopleCounts inv; inv[GoodType::Fish] = 40; @@ -68,31 +54,38 @@ struct MineProductionFixture : WorldWithGCExecution1P world.GetSpecObj(hqPos)->AddToInventory(inv, true); } - const nobUsual* PlaceMine(const BuildingType mineType, MapPoint& minePos) + // Places a mine of the given type next to the HQ, optionally seeds its resource spot, connects it by road and + // waits until the miner has moved in. Returns the mine position. + MapPoint CreateMine(const BuildingType mineType, const Resource initialResource = Resource()) { - minePos = hqPos + MapPoint(2, 0); - return static_cast( + const MapPoint minePos = hqPos + MapPoint(2, 0); + const auto* mine = static_cast( BuildingFactory::CreateBuilding(world, mineType, minePos, curPlayer, Nation::Romans)); - } - - void ConnectMineAndWaitForWorker(const MapPoint minePos, const nobUsual* mine) - { + if(initialResource.getType() != ResourceType::Nothing) + world.GetNodeWriteable(minePos).resources = initialResource; BuildRoad(world.GetNeighbour(minePos, Direction::SouthEast), false, std::vector(2, Direction::West)); RTTR_EXEC_TILL(500, mine->HasWorker()); + return minePos; } - MapPoint CreateMine(const BuildingType mineType, const Resource initialResource = Resource()) + static void SeedProductionRng(const unsigned seed) { RANDOM.Init(seed); } +}; + +// One S4-like "no output" fallback scenario: a nearly exhausted mine that keeps working but usually mines nothing, +// so the configured fallback ware is produced instead of the primary ware. +struct NoOutputFallbackCase +{ + AddonId mineBehaviorAddon; + BuildingType mineType; + ResourceType mineResource; + GoodType primaryGood; // must stay unchanged (deposit too small to actually mine) + MineNoOutputFallback fallback; + GoodType fallbackGood; // must be produced instead + unsigned seed; // chosen so the (probabilistic) fallback fires within maxProductionGFs + friend std::ostream& operator<<(std::ostream& os, const NoOutputFallbackCase& c) { - AddMinerSupplies(); - MapPoint minePos; - const nobUsual* mine = PlaceMine(mineType, minePos); - if(initialResource.getType() != ResourceType::Nothing) - world.GetNodeWriteable(minePos).resources = initialResource; - ConnectMineAndWaitForWorker(minePos, mine); - return minePos; + return os << "fallback=" << static_cast(c.fallback); } - - void ResetMineProductionRng(const unsigned seed) { RANDOM.Init(seed); } }; } // namespace @@ -175,38 +168,41 @@ BOOST_FIXTURE_TEST_CASE(MetalWorkerOrders, WorldWithGCExecution1P) RTTR_EXEC_TILL(1300, mw->is_working); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithoutResourcesFixture) +// Without any deposit under the mine, only the WorkEverywhere behavior lets it produce; Default and Inexhaustible +// both keep needing an actual resource spot. Depletion/production here is deterministic, so no RNG seed is needed. +BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, MineProductionFixture) { - CreateGraniteMineWithoutResources(); + CreateMine(BuildingType::GraniteMine); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_SKIP_GFS(2000); + RTTR_SKIP_GFS(maxProductionGFs); BOOST_TEST(curInventory[GoodType::Stones] == initialStones); } -BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteMineStillNeedsResourceSpot, MineProductionFixture) { - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, 1); - CreateGraniteMineWithoutResources(); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); + CreateMine(BuildingType::GraniteMine); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_SKIP_GFS(2000); + RTTR_SKIP_GFS(maxProductionGFs); BOOST_TEST(curInventory[GoodType::Stones] == initialStones); } -BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource, GraniteMineWithoutResourcesFixture) +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereProducesWithoutCreatingResource, MineProductionFixture) { ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); - const MapPoint minePos = CreateGraniteMineWithoutResources(); + const MapPoint minePos = CreateMine(BuildingType::GraniteMine); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Stones] > initialStones); + // WorkEverywhere must not conjure a deposit into the ground BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) == static_cast(ResourceType::Nothing)); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); @@ -216,27 +212,34 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereIgnoresExistingResource, MinePr { ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::WorkEverywhere)); - const MapPoint minePos = CreateMine(BuildingType::GraniteMine, Resource(ResourceType::Coal, 4)); + // A granite mine on a foreign (coal) deposit still just makes stones and leaves the deposit untouched. + const Resource foreignDeposit(ResourceType::Coal, 4); + const MapPoint minePos = CreateMine(BuildingType::GraniteMine, foreignDeposit); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); - BOOST_TEST(world.GetNode(minePos).resources.has(ResourceType::Coal)); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 4u); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Stones] > initialStones); + BOOST_TEST(world.GetNode(minePos).resources.has(foreignDeposit.getType())); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == foreignDeposit.getAmount()); } BOOST_FIXTURE_TEST_CASE(CoalMineInexhaustibleBehaviorDoesNotDepleteResource, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); - const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 4)); + const Resource initCoal(ResourceType::Coal, 4); + const MapPoint minePos = CreateMine(BuildingType::CoalMine, initCoal); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialCoal = curInventory[GetMineGoodType(BuildingType::CoalMine)]; + const unsigned initialCoal = curInventory[GetMineOutput(BuildingType::CoalMine)]; - RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Coal] > initialCoal); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 4u); + // Inexhaustible mines produce without ever reducing the deposit + BOOST_TEST(world.GetNode(minePos).resources.has(initCoal.getType())); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == initCoal.getAmount()); } +// Comparison A: an almost exhausted S4-like mine mostly mines nothing, so the coal count stays put (see the +// s4LikeComparison* constants). Comparison B is CoalMineS4LikeExhaustionReducesResourceOnSuccessfulCycle below. BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionCanProduceNothing, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, @@ -245,94 +248,61 @@ BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionCanProduceNothing, MineProductio const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialCoal = curInventory[GoodType::Coal]; - ResetMineProductionRng(2); - RTTR_SKIP_GFS(2000); - - BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); -} - -BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback25ProducesStones, MineProductionFixture) -{ - ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, - static_cast(MineResourceBehavior::S4LikeExhaustion)); - ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite25)); - const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); - const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialCoal = curInventory[GoodType::Coal]; - const unsigned initialStones = curInventory[GoodType::Stones]; - - ResetMineProductionRng(2); - RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); + SeedProductionRng(s4LikeComparisonSeed); + RTTR_SKIP_GFS(s4LikeComparisonGFs); BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); } -BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback50ProducesStones, MineProductionFixture) +// Same scenario for every configured no-output fallback: an S4-like mine sitting on a single-unit deposit keeps +// working but (almost) never mines it, so the fallback ware appears while the primary ware and the deposit are +// untouched. The seeds are per-case because the fallback chance is probabilistic (25%/50% need a matching roll). +BOOST_DATA_TEST_CASE_F(MineProductionFixture, S4LikeNoOutputFallbackProducesFallbackWare, + dataset::make(std::array{ + NoOutputFallbackCase{AddonId::COALMINE_RESOURCE_BEHAVIOR, BuildingType::CoalMine, + ResourceType::Coal, GoodType::Coal, + MineNoOutputFallback::ProduceGranite25, GoodType::Stones, 2}, + NoOutputFallbackCase{AddonId::COALMINE_RESOURCE_BEHAVIOR, BuildingType::CoalMine, + ResourceType::Coal, GoodType::Coal, + MineNoOutputFallback::ProduceGranite50, GoodType::Stones, 7}, + NoOutputFallbackCase{AddonId::COALMINE_RESOURCE_BEHAVIOR, BuildingType::CoalMine, + ResourceType::Coal, GoodType::Coal, + MineNoOutputFallback::ProduceGranite100, GoodType::Stones, 2}, + NoOutputFallbackCase{AddonId::GOLDMINE_RESOURCE_BEHAVIOR, BuildingType::GoldMine, + ResourceType::Gold, GoodType::Gold, + MineNoOutputFallback::ProduceLowerGradeResource, GoodType::IronOre, 2}})) { - ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, - static_cast(MineResourceBehavior::S4LikeExhaustion)); - ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite50)); - const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); - const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialCoal = curInventory[GoodType::Coal]; - const unsigned initialStones = curInventory[GoodType::Stones]; - - ResetMineProductionRng(7); - RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); - - BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); -} - -BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeNoOutputGraniteFallback100ProducesStones, MineProductionFixture) -{ - ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, - static_cast(MineResourceBehavior::S4LikeExhaustion)); - ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(MineNoOutputFallback::ProduceGranite100)); - const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 1)); - const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialCoal = curInventory[GoodType::Coal]; - const unsigned initialStones = curInventory[GoodType::Stones]; - - ResetMineProductionRng(2); - RTTR_EXEC_TILL(2000, curInventory[GoodType::Stones] > initialStones); - - BOOST_TEST(curInventory[GoodType::Coal] == initialCoal); - BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); -} - -BOOST_FIXTURE_TEST_CASE(GoldMineS4LikeNoOutputLowerGradeFallbackProducesIronOre, MineProductionFixture) -{ - ggs.setSelection(AddonId::GOLDMINE_RESOURCE_BEHAVIOR, - static_cast(MineResourceBehavior::S4LikeExhaustion)); - ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, - static_cast(MineNoOutputFallback::ProduceLowerGradeResource)); - const MapPoint minePos = CreateMine(BuildingType::GoldMine, Resource(ResourceType::Gold, 1)); + ggs.setSelection(sample.mineBehaviorAddon, static_cast(MineResourceBehavior::S4LikeExhaustion)); + ggs.setSelection(AddonId::MINE_NO_OUTPUT_FALLBACK, static_cast(sample.fallback)); + const MapPoint minePos = CreateMine(sample.mineType, Resource(sample.mineResource, 1)); const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); - const unsigned initialGold = curInventory[GoodType::Gold]; - const unsigned initialIronOre = curInventory[GoodType::IronOre]; + const unsigned initialPrimary = curInventory[sample.primaryGood]; + const unsigned initialFallback = curInventory[sample.fallbackGood]; - ResetMineProductionRng(2); - RTTR_EXEC_TILL(2000, curInventory[GoodType::IronOre] > initialIronOre); + SeedProductionRng(sample.seed); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[sample.fallbackGood] > initialFallback); - BOOST_TEST(curInventory[GoodType::Gold] == initialGold); + BOOST_TEST(curInventory[sample.primaryGood] == initialPrimary); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 1u); } +// Comparison B (see CoalMineS4LikeExhaustionCanProduceNothing): same seed and window, but a full deposit reliably +// completes a producing cycle, which reduces the deposit by one (down to, but never below, the minimum of 1). BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeExhaustionReducesResourceOnSuccessfulCycle, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::S4LikeExhaustion)); const MapPoint minePos = CreateMine(BuildingType::CoalMine, Resource(ResourceType::Coal, 15)); - ResetMineProductionRng(21); - RTTR_EXEC_TILL(5000, world.GetNode(minePos).resources.getAmount() == 14u); + SeedProductionRng(s4LikeComparisonSeed); + RTTR_EXEC_TILL(s4LikeComparisonGFs, world.GetNode(minePos).resources.getAmount() == 14u); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 14u); } +// A mine that actually produces its primary ware ignores the no-output fallback entirely (no stones), both for the +// S4-like and the default behavior. The two are structured identically and only differ in how the deposit is used. BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeSuccessfulCycleIgnoresNoOutputFallback, MineProductionFixture) { ggs.setSelection(AddonId::COALMINE_RESOURCE_BEHAVIOR, @@ -343,10 +313,12 @@ BOOST_FIXTURE_TEST_CASE(CoalMineS4LikeSuccessfulCycleIgnoresNoOutputFallback, Mi const unsigned initialCoal = curInventory[GoodType::Coal]; const unsigned initialStones = curInventory[GoodType::Stones]; - ResetMineProductionRng(21); - RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + // Seed chosen so the very first cycle succeeds: otherwise a failed cycle would emit a fallback stone first. + SeedProductionRng(21); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Coal] > initialCoal); BOOST_TEST(curInventory[GoodType::Stones] == initialStones); + // S4-like consumes exactly one unit per successful cycle BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 14u); } @@ -358,9 +330,11 @@ BOOST_FIXTURE_TEST_CASE(CoalMineDefaultProductionIgnoresNoOutputFallback, MinePr const unsigned initialCoal = curInventory[GoodType::Coal]; const unsigned initialStones = curInventory[GoodType::Stones]; - RTTR_EXEC_TILL(5000, curInventory[GoodType::Coal] > initialCoal); + // Default production is deterministic (always mines when a deposit is present), so no seed is needed. + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Coal] > initialCoal); BOOST_TEST(curInventory[GoodType::Stones] == initialStones); + // The default behavior depletes the deposit on every production BOOST_TEST(world.GetNode(minePos).resources.getAmount() < 3u); } @@ -371,7 +345,7 @@ BOOST_FIXTURE_TEST_CASE(CoalMineWorkEverywhereBehaviorProducesWithoutCreatingRes const Inventory& curInventory = world.GetPlayer(curPlayer).GetInventory(); const unsigned initialCoal = curInventory[GoodType::Coal]; - RTTR_EXEC_TILL(2000, curInventory[GoodType::Coal] > initialCoal); + RTTR_EXEC_TILL(maxProductionGFs, curInventory[GoodType::Coal] > initialCoal); BOOST_TEST(static_cast(world.GetNode(minePos).resources.getType()) == static_cast(ResourceType::Nothing)); BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); diff --git a/tests/s25Main/integration/testSerialization.cpp b/tests/s25Main/integration/testSerialization.cpp index 0a7c966b11..dd8367b293 100644 --- a/tests/s25Main/integration/testSerialization.cpp +++ b/tests/s25Main/integration/testSerialization.cpp @@ -236,9 +236,10 @@ BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeMigratesToPerMineBehavio BOOST_AUTO_TEST_CASE(LegacyInexhaustibleMinesDeserializeAlsoMigratesLegacyGraniteAddonValue) { - // Old data always contains a value for 0x00800000 (formerly INEXHAUSTIBLE_GRANITEMINES, now - // GRANITEMINE_RESOURCE_BEHAVIOR). A disabled granite addon must not prevent the legacy global setting from - // making granite mines inexhaustible, which is what the old code did. + // Old data always contains an explicit (usually disabled) value for GRANITEMINE_RESOURCE_BEHAVIOR, because that + // id reuses the slot of the former INEXHAUSTIBLE_GRANITEMINES addon which was always serialized. Before this + // refactor the separate INEXHAUSTIBLE_MINES setting turned granite mines inexhaustible on its own, so migration + // must not let a disabled granite value suppress that: granite still becomes Inexhaustible here. Serializer ser; PushSerializedGGSHeader(ser); ser.PushUnsignedInt(2); From 86d6f7a6358c2f38c19bd45cfbaf3e909e052696 Mon Sep 17 00:00:00 2001 From: mries Date: Thu, 30 Jul 2026 13:13:04 +0200 Subject: [PATCH 22/23] fix: split combined AI test, increase S4-like GF window to 5000 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split GraniteMineResourceBehaviorAffectsAIMineSearch into two tests because the AI caches GGS on construction; Inexhaustible needs its own test with post-selection construction. - Bump s4LikeComparisonGFs 2000→5000 so the resource-depletion cycle reliably completes within the window. - Fix Boost.Test iterator-printing error in testGamePlayer.cpp. --- tests/s25Main/integration/testAI.cpp | 15 ++++++++++++++- tests/s25Main/integration/testGamePlayer.cpp | 2 +- tests/s25Main/integration/testProduction.cpp | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index c69296d987..4c0ce61e6b 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -209,9 +209,22 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineResourceBehaviorAffectsAIMineSearch, EmptyWor // Switching to Inexhaustible does NOT imply "work everywhere": without an actual deposit the AI finds no spot. ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); - BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); } +BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteDoesNotImplyWorkEverywhereForAI, EmptyWorldFixture1P) +{ + // Same setup as above, but the AI is constructed AFTER Inexhaustible is selected. + // Unlike WorkEverywhere, Inexhaustible requires an actual deposit. + makeWorldMineable(world); + ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, + static_cast(MineResourceBehavior::Inexhaustible)); + + AIJH::AIPlayerJH ai(0, world, AI::Level::Hard); + initAIJhNodes(ai, world, 0); + const MapPoint searchCenter = world.GetPlayer(0).GetHQPos(); + + BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); +} BOOST_FIXTURE_TEST_CASE(KeepBQUpdated, BiggerWorldWithGCExecution) { addStartResources(); diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 7b1c9b2d77..8531bfad95 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -216,7 +216,7 @@ BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius const MapPoint outOfRangePt = world.GetNeighbour( world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); const auto inRangePts = world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS); - BOOST_TEST_REQUIRE(std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end()); + const bool outOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); BOOST_TEST_REQUIRE(outOfRange); world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 0u); BOOST_TEST(coalMine->GetProductivity() == 0u); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index e2205487c4..16c312c762 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -43,7 +43,7 @@ struct MineProductionFixture : WorldWithGCExecution1P // sequence, a near-exhausted mine (1 resource -> ~5% chance) produces nothing within the window, while a full // mine (many resources -> high chance) reliably completes a depleting cycle. Only the resource amount differs. static constexpr unsigned s4LikeComparisonSeed = 2; - static constexpr unsigned s4LikeComparisonGFs = 2000; + static constexpr unsigned s4LikeComparisonGFs = 5000; MineProductionFixture() { From 81c1ac187e62eeb743375382a105dc318075fe2b Mon Sep 17 00:00:00 2001 From: mries Date: Thu, 30 Jul 2026 13:24:40 +0200 Subject: [PATCH 23/23] chore: split BOOST_TEST_REQUIRE line, remove dead setSelection in AI test --- tests/s25Main/integration/testAI.cpp | 2 -- tests/s25Main/integration/testGamePlayer.cpp | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/s25Main/integration/testAI.cpp b/tests/s25Main/integration/testAI.cpp index 4c0ce61e6b..16fadab134 100644 --- a/tests/s25Main/integration/testAI.cpp +++ b/tests/s25Main/integration/testAI.cpp @@ -207,8 +207,6 @@ BOOST_FIXTURE_TEST_CASE(GraniteMineResourceBehaviorAffectsAIMineSearch, EmptyWor BOOST_TEST(ai.FindBestPosition(searchCenter, AIResource::Granite, BuildingQuality::Mine, 5).isValid()); BOOST_TEST(!ai.FindBestPosition(searchCenter, AIResource::Coal, BuildingQuality::Mine, 5).isValid()); - // Switching to Inexhaustible does NOT imply "work everywhere": without an actual deposit the AI finds no spot. - ggs.setSelection(AddonId::GRANITEMINE_RESOURCE_BEHAVIOR, static_cast(MineResourceBehavior::Inexhaustible)); } BOOST_FIXTURE_TEST_CASE(InexhaustibleGraniteDoesNotImplyWorkEverywhereForAI, EmptyWorldFixture1P) diff --git a/tests/s25Main/integration/testGamePlayer.cpp b/tests/s25Main/integration/testGamePlayer.cpp index 8531bfad95..9c60f1ef19 100644 --- a/tests/s25Main/integration/testGamePlayer.cpp +++ b/tests/s25Main/integration/testGamePlayer.cpp @@ -216,7 +216,8 @@ BOOST_FIXTURE_TEST_CASE(MineProductivityUsesAllMatchingResourcesWithinMineRadius const MapPoint outOfRangePt = world.GetNeighbour( world.GetNeighbour(world.GetNeighbour(minePos, Direction::East), Direction::East), Direction::East); const auto inRangePts = world.GetPointsInRadiusWithCenter(minePos, MINER_RADIUS); - const bool outOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); BOOST_TEST_REQUIRE(outOfRange); + const bool outOfRange = std::find(inRangePts.begin(), inRangePts.end(), outOfRangePt) == inRangePts.end(); + BOOST_TEST_REQUIRE(outOfRange); world.SetResource(outOfRangePt, Resource(ResourceType::Coal, 15)); BOOST_TEST(GetRemainingMineResources(world, minePos, ResourceType::Coal) == 0u); BOOST_TEST(coalMine->GetProductivity() == 0u);