diff --git a/libs/s25main/BuildingRegister.cpp b/libs/s25main/BuildingRegister.cpp index 13a5e8094b..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); @@ -155,20 +170,26 @@ 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)) 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; - } - 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 bcc5fb488c..003133e130 100644 --- a/libs/s25main/BuildingRegister.h +++ b/libs/s25main/BuildingRegister.h @@ -41,6 +41,8 @@ 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/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index f5047f9893..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,7 +108,10 @@ void GlobalGameSettings::registerAllAddons() AddonHalfCostMilEquip, AddonInexhaustibleFish, AddonInexhaustibleGraniteMines, - AddonInexhaustibleMines, + AddonCoalMineResourceBehavior, + AddonIronMineResourceBehavior, + AddonGoldMineResourceBehavior, + AddonMineNoOutputFallback, AddonLimitCatapults, AddonManualRoadEnlargement, AddonMaxRank, @@ -187,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); } /** @@ -243,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/AddonInexhaustibleGraniteMines.h b/libs/s25main/addons/AddonInexhaustibleGraniteMines.h index 418d30e791..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 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 be depleted.")) + : 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/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..5ae0e9c960 --- /dev/null +++ b/libs/s25main/addons/AddonMineResourceBehavior.h @@ -0,0 +1,48 @@ +// 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"), _("Inexhaustible"), _("S4-like exhaustion"), _("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.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 96b3203ff7..8bbc7c68c4 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -37,6 +37,8 @@ #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..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,7 +58,9 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x MILITARY_AID = 0x00700000, - INEXHAUSTIBLE_GRANITEMINES = 0x00800000, + 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 8a2a59f042..fe714702e0 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,14 @@ 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; } + +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 +167,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(subres.getType()); + if(mineBuildingType + && GetMineResourceBehavior(gwb.GetGGS(), *mineBuildingType) + == MineResourceBehavior::S4LikeExhaustion) + return GetS4LikeMineResourceRating(subres, RES_RADIUS[res]); + + return RES_RADIUS[res]; + } + if(IsMineResourceWorkEverywhere(res) + && 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 +192,17 @@ int AIInterface::GetResourceRating(const MapPoint pt, AIResource res) const return 0; } +bool AIInterface::IsMineResourceWorkEverywhere(const AIResource res) const +{ + const auto resourceType = convertToResourceType(res); + if(!resourceType) + return false; + + const auto mineBuildingType = GetMineBuildingType(*resourceType); + return mineBuildingType + && GetMineResourceBehavior(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/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 72fbee111f..78eacd5263 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; } @@ -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 9ed430b514..39ff3acddd 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_) + || 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(); + + 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..2cfe2af638 100644 --- a/libs/s25main/figures/nofMiner.cpp +++ b/libs/s25main/figures/nofMiner.cpp @@ -10,7 +10,104 @@ #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; + +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); +} + +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); +} + +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 +157,22 @@ unsigned short nofMiner::GetCarryID() const helpers::OptionalEnum nofMiner::ProduceWare() { + const GlobalGameSettings& settings = world->GetGGS(); + const MineResourceBehavior behavior = GetMineResourceBehavior(settings, workplace->GetBuildingType()); + + 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 >= GetS4LikeProductionChanceForRemainingResources(*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; @@ -71,30 +184,39 @@ helpers::OptionalEnum nofMiner::ProduceWare() bool nofMiner::AreWaresAvailable() const { - return nofWorkman::AreWaresAvailable() && FindPointWithResource(GetRequiredResType()).isValid(); + if(!nofWorkman::AreWaresAvailable()) + return false; + + const MineResourceBehavior behavior = GetMineResourceBehavior(world->GetGGS(), workplace->GetBuildingType()); + if(behavior == MineResourceBehavior::WorkEverywhere) + return true; + + if(FindPointWithResource(GetRequiredResType(), false).isValid()) + return true; + + workplace->OnOutOfResources(); + return false; } bool nofMiner::StartWorking() { - MapPoint resPt = FindPointWithResource(GetRequiredResType()); + const GlobalGameSettings& settings = world->GetGGS(); + const MineResourceBehavior behavior = GetMineResourceBehavior(settings, workplace->GetBuildingType()); + if(behavior == MineResourceBehavior::WorkEverywhere) + return nofWorkman::StartWorking(); + + const 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) + + if(behavior != 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/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/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..a9650e6ede --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.cpp @@ -0,0 +1,80 @@ +// 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_FULL_PRODUCTIVITY_RESOURCE_AMOUNT = 20; +} // 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::INEXHAUSTIBLE_GRANITEMINES; + } +} + +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; + } +} + +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 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(); + return std::min(MAX_PRODUCTION_PERCENT, chancePercent); +} + +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; + } +} + +bool IsMineResourceDepletable(const GlobalGameSettings& settings, const BuildingType buildingType) +{ + 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 new file mode 100644 index 0000000000..b86f03fc73 --- /dev/null +++ b/libs/s25main/gameTypes/MineResourceBehavior.h @@ -0,0 +1,30 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "helpers/OptionalEnum.h" +#include + +class GlobalGameSettings; +enum class AddonId; +enum class BuildingType : unsigned char; +enum class ResourceType : uint8_t; + +enum class MineResourceBehavior : unsigned +{ + Default = 0, + Inexhaustible = 1, + S4LikeExhaustion = 2, + WorkEverywhere = 3 +}; + +AddonId GetMineResourceBehaviorAddonId(BuildingType buildingType); +ResourceType GetMineResourceType(BuildingType buildingType); +helpers::OptionalEnum GetMineBuildingType(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); 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..9279b7d9e5 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,65 @@ 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(); + 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(GraniteWorkEverywhereAffectsGraniteOnly, EmptyWorldFixture1P) +{ + makeWorldMineable(world); + ggs.setSelection(AddonId::INEXHAUSTIBLE_GRANITEMINES, 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()); +} + +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..53d6777fa2 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,204 @@ 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(GraniteMineWorkEverywhereProducesWithoutCreatingResource, GraniteMineWithoutResourcesFixture) +{ + 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(static_cast(world.GetNode(minePos).resources.getType()) + == static_cast(ResourceType::Nothing)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); +} + +BOOST_FIXTURE_TEST_CASE(GraniteMineWorkEverywhereIgnoresExistingResource, MineProductionFixture) +{ + 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::Coal)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 4u); +} + +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(CoalMineWorkEverywhereBehaviorProducesWithoutCreatingResource, 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(static_cast(world.GetNode(minePos).resources.getType()) + == static_cast(ResourceType::Nothing)); + BOOST_TEST(world.GetNode(minePos).resources.getAmount() == 0u); +} + BOOST_AUTO_TEST_SUITE_END() 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;