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 418d30e791..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 to have unlimited 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 will never be depleted.")) + _("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 912619b58a..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,25 +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() && FindPointWithResource(GetRequiredResType()).isValid(); + if(!nofWorkman::AreWaresAvailable()) + return false; + + if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() || CanCreateWorkEverywhereGraniteResource()) + return true; + + workplace->OnOutOfResources(); + return false; } 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)); + MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); + if(!resPt.isValid()) + { + resPt = CreateWorkEverywhereGraniteResource(); + if(!resPt.isValid()) + { + workplace->OnOutOfResources(); + return false; + } + } + + 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 c3fdc810ad..b45fa765ea 100644 --- a/libs/s25main/figures/nofMiner.h +++ b/libs/s25main/figures/nofMiner.h @@ -23,6 +23,9 @@ class nofMiner : public nofWorkman bool AreWaresAvailable() const override; 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/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 5ce05b76b4..ba54e84ce1 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 +{ + 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; + } +}; +} // namespace + BOOST_FIXTURE_TEST_CASE(MetalWorkerStopped, WorldWithGCExecution1P) { addStartResources(); @@ -102,4 +123,61 @@ BOOST_FIXTURE_TEST_CASE(MetalWorkerOrders, WorldWithGCExecution1P) RTTR_EXEC_TILL(1300, mw->is_working); } +BOOST_FIXTURE_TEST_CASE(GraniteMineWithoutResourcesNeedsAddon, GraniteMineWithoutResourcesFixture) +{ + 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(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()