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/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index f5047f9893..2094af1037 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -79,6 +79,12 @@ void GlobalGameSettings::registerAllAddons() AddonHalfCostMilEquip, AddonInexhaustibleFish, AddonInexhaustibleGraniteMines, + AddonGraniteMinesWorkEverywhere, + AddonCoalMineResourceBehavior, + AddonIronMineResourceBehavior, + AddonGoldMineResourceBehavior, + AddonGraniteMineResourceBehavior, + AddonMineNoOutputFallback, 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/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/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 96b3203ff7..43d1580afd 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -33,10 +33,13 @@ #include "addons/AddonToolOrdering.h" +#include "addons/AddonGraniteMinesWorkEverywhere.h" #include "addons/AddonInexhaustibleFish.h" #include "addons/AddonInexhaustibleGraniteMines.h" #include "addons/AddonMaxRank.h" #include "addons/AddonMilitaryAid.h" +#include "addons/AddonMineNoOutputFallback.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 f7ddf54f08..fd32afa694 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -57,7 +57,10 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, - INEXHAUSTIBLE_GRANITEMINES = 0x00800000, + 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/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/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 912619b58a..f971860d1a 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -10,7 +10,124 @@ #include "buildings/nobUsual.h" #include "network/GameClient.h" #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" +#include +#include + +namespace { +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) +{ + 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 GetS4LikeProductionChance(const GameWorld& world, const std::vector& resourcePts) +{ + unsigned resourceAmount = 0; + for(const MapPoint pt : resourcePts) + resourceAmount += world.GetNode(pt).resources.getAmount(); + + return GetS4LikeMineProductionChance(resourceAmount); +} + +unsigned GetGraniteFallbackChance(const MineNoOutputFallback fallback) +{ + switch(fallback) + { + case MineNoOutputFallback::ProduceGranite25: return GRANITE_FALLBACK_25_PERCENT; + case MineNoOutputFallback::ProduceGranite50: return GRANITE_FALLBACK_50_PERCENT; + case MineNoOutputFallback::ProduceGranite100: return MAX_PRODUCTION_PERCENT; + default: return 0; + } +} + +helpers::OptionalEnum 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>( + 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) @@ -60,6 +177,23 @@ unsigned short nofMiner::GetCarryID() const helpers::OptionalEnum nofMiner::ProduceWare() { + const GlobalGameSettings& settings = world->GetGGS(); + const MineResourceBehavior effectiveBehavior = + GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); + + 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 GetNoOutputFallbackGood(settings, workplace->GetBuildingType(), GetObjId()); + + if(IsMineResourceDepletable(settings, workplace->GetBuildingType())) + ReduceS4LikeResource(*world, resourcePts); + } + switch(workplace->GetBuildingType()) { case BuildingType::GoldMine: return GoodType::Gold; @@ -69,32 +203,52 @@ 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 { - return nofWorkman::AreWaresAvailable() && FindPointWithResource(GetRequiredResType()).isValid(); + if(!nofWorkman::AreWaresAvailable()) + return false; + + const MineResourceBehavior effectiveBehavior = + GetEffectiveMineResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); + + if(FindPointWithResourceQuiet(GetRequiredResType()).isValid() + || CanCreateWorkEverywhereResource(*world, pos, effectiveBehavior)) + 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)); - if(!inexhaustibleRes) + const MineResourceBehavior effectiveBehavior = + GetEffectiveMineResourceBehavior(settings, workplace->GetBuildingType()); + MapPoint resPt = FindPointWithResourceQuiet(GetRequiredResType()); + if(!resPt.isValid()) + { + resPt = CreateWorkEverywhereResource(*world, pos, GetRequiredResType(), effectiveBehavior, GetObjId()); + if(!resPt.isValid()) + { + workplace->OnOutOfResources(); + return false; + } + } + + if(effectiveBehavior != MineResourceBehavior::S4LikeExhaustion + && IsMineResourceDepletable(settings, workplace->GetBuildingType())) world->ReduceResource(resPt); + return nofWorkman::StartWorking(); } ResourceType nofMiner::GetRequiredResType() const { - switch(workplace->GetBuildingType()) - { - 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/figures/nofMiner.h b/libs/s25main/figures/nofMiner.h index c3fdc810ad..b13cf9a736 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; + MapPoint FindPointWithResourceQuiet(ResourceType type) const; public: nofMiner(MapPoint pos, unsigned char player, nobUsual* workplace); 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/libs/s25main/gameTypes/MineResourceBehavior.cpp b/libs/s25main/gameTypes/MineResourceBehavior.cpp new file mode 100644 index 0000000000..7b41468c14 --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -0,0 +1,88 @@ +// 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" +#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) +{ + 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; + } +} + +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) +{ + 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 new file mode 100644 index 0000000000..0e6914e00d --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -0,0 +1,27 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class GlobalGameSettings; +enum class AddonId; +enum class BuildingType : unsigned char; +enum class ResourceType : uint8_t; + +enum class MineResourceBehavior : unsigned +{ + Default, + S4LikeExhaustion, + Inexhaustible, + WorkEverywhere +}; + +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/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(); 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); diff --git a/tests/s25Main/integration/testProduction.cpp b/tests/s25Main/integration/testProduction.cpp index 5ce05b76b4..df6a8b3c89 100644 --- a/tests/s25Main/integration/testProduction.cpp +++ b/tests/s25Main/integration/testProduction.cpp @@ -7,7 +7,10 @@ #include "factories/BuildingFactory.h" #include "postSystem/PostBox.h" #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 #include @@ -23,6 +26,76 @@ 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; + } +}; + +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) { addStartResources(); @@ -102,4 +175,217 @@ 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_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(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, + 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(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)); + 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()