Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ea2ee5f
Fix inexhaustible granite mine production
DevOpsOfChaos May 1, 2026
b887aaf
Simplify granite mine resource handling
DevOpsOfChaos May 1, 2026
4e4b9d1
Tighten granite mine production tests
DevOpsOfChaos May 1, 2026
b636b5d
Clarify inexhaustible granite mines addon description
DevOpsOfChaos May 1, 2026
2a4936e
Avoid runtime addon toggle in granite mine test
DevOpsOfChaos May 1, 2026
fb40fc9
Split granite mine work-everywhere addon
DevOpsOfChaos May 1, 2026
44bbab0
Implement configurable mine resource behavior
DevOpsOfChaos May 14, 2026
64a8b71
Implement mine no-output fallback behavior
DevOpsOfChaos May 15, 2026
621d52e
Teach AI mine planning configurable resource behavior
DevOpsOfChaos May 15, 2026
d1df485
Adjust mine productivity display for S4-like behavior
DevOpsOfChaos May 15, 2026
1733b37
Merge branch 'master' into sidequest/configurable-mine-resource-behavior
DevOpsOfChaos May 21, 2026
b9be39f
Merge branch 'master' into sidequest/configurable-mine-resource-behavior
DevOpsOfChaos May 26, 2026
bf24106
Rework configurable mine resource behavior
DevOpsOfChaos May 26, 2026
86587eb
Clarify S4-like mine productivity reference
DevOpsOfChaos May 26, 2026
3c27b7e
Merge remote-tracking branch 'upstream/master' into sidequest/configu…
DevOpsOfChaos Jun 23, 2026
9535c6c
Address mine resource behavior review feedback
DevOpsOfChaos Jun 23, 2026
1bade00
Fix mine resource behavior review regression
DevOpsOfChaos Jun 23, 2026
ea0a195
Merge remote-tracking branch 'upstream/master' into sidequest/configu…
DevOpsOfChaos Jun 23, 2026
e17142e
Harden mine resource behavior coverage
DevOpsOfChaos Jun 23, 2026
2cb7660
Polish mine resource behavior compatibility
DevOpsOfChaos Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libs/common/include/helpers/MaxEnumValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ inline constexpr unsigned MaxEnumValue_v = static_cast<std::underlying_type_t<T_
template<class T_Enum>
inline constexpr unsigned NumEnumValues_v = MaxEnumValue_v<T_Enum> + 1u;

/// Check whether a numeric value is a valid enumerator for an enum type
template<class T_Enum>
inline constexpr bool isValidEnumValue(unsigned value)
{
return value <= MaxEnumValue_v<T_Enum>;
}

} // namespace helpers
26 changes: 16 additions & 10 deletions libs/s25main/BuildingRegister.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@
#include "gameData/BuildingConsts.h"
#include "gameData/BuildingProperties.h"

namespace {
unsigned CalcAverageProductivity(const std::list<nobUsual*>& 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);
Expand Down Expand Up @@ -159,16 +174,7 @@ unsigned BuildingRegister::CalcAverageProductivity(BuildingType bldType) const
{
if(holds_alternative<boost::none_t>(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
Expand Down
66 changes: 64 additions & 2 deletions libs/s25main/GlobalGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <boost/mp11/algorithm.hpp>
#include <boost/mp11/list.hpp>
#include <algorithm>
#include <array>
#include <iostream>
#include <stdexcept>

namespace {
constexpr std::array<BuildingType, 4> MINE_BUILDING_TYPES = {BuildingType::GraniteMine, BuildingType::CoalMine,
BuildingType::IronMine, BuildingType::GoldMine};

helpers::OptionalEnum<BuildingType> 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<bool, BuildingType>& hasMineBehaviorSetting)
{
for(const BuildingType mineType : MINE_BUILDING_TYPES)
{
if(!hasMineBehaviorSetting[mineType])
settings.setSelection(GetMineResourceBehaviorAddonId(mineType),
static_cast<unsigned>(MineResourceBehavior::Inexhaustible));
}
}
} // namespace

GlobalGameSettings::GlobalGameSettings()
: speed(GameSpeed::Normal), objective(GameObjective::None), startWares(StartWares::Normal), lockedTeams(false),
exploration(Exploration::FogOfWar), teamView(true), randomStartPosition(false)
Expand Down Expand Up @@ -79,7 +108,10 @@ void GlobalGameSettings::registerAllAddons()
AddonHalfCostMilEquip,
AddonInexhaustibleFish,
AddonInexhaustibleGraniteMines,
AddonInexhaustibleMines,
AddonCoalMineResourceBehavior,
AddonIronMineResourceBehavior,
AddonGoldMineResourceBehavior,
AddonMineNoOutputFallback,
AddonLimitCatapults,
AddonManualRoadEnlargement,
AddonMaxRank,
Expand Down Expand Up @@ -187,8 +219,25 @@ void GlobalGameSettings::LoadSettings()
{
resetAddons();

bool migrateLegacyInexhaustibleMines = false;
helpers::EnumArray<bool, BuildingType> hasMineBehaviorSetting{};
for(const auto& it : SETTINGS.addons.configuration)
setSelection(static_cast<AddonId>(it.first), it.second);
{
const auto id = static_cast<AddonId>(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);
}

/**
Expand Down Expand Up @@ -243,12 +292,25 @@ void GlobalGameSettings::Deserialize(Serializer& ser)

resetAddons();

bool migrateLegacyInexhaustibleMines = false;
helpers::EnumArray<bool, BuildingType> hasMineBehaviorSetting{};
for(unsigned i = 0; i < count; ++i)
{
auto addon = static_cast<AddonId>(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)
Expand Down
13 changes: 8 additions & 5 deletions libs/s25main/addons/AddonInexhaustibleGraniteMines.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this class to the other ones to avoid missing it. And you can rename the addon id to match the new ones if you keep the value.

{
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."))
{}
};
6 changes: 5 additions & 1 deletion libs/s25main/addons/AddonInexhaustibleMines.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
#include "mygettext/mygettext.h"

/**
* Addon for allowing to have unlimited resources.
* Deprecated global mine setting.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add TODO(Replay) TODO(Savegame) so it can be found when updating

*
* 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
{
Expand Down
22 changes: 22 additions & 0 deletions libs/s25main/addons/AddonMineNoOutputFallback.h
Original file line number Diff line number Diff line change
@@ -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<unsigned>(MineNoOutputFallback::ProduceNothing))
{}
};
48 changes: 48 additions & 0 deletions libs/s25main/addons/AddonMineResourceBehavior.h
Original file line number Diff line number Diff line change
@@ -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 <string>

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<unsigned>(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."))
{}
};
2 changes: 2 additions & 0 deletions libs/s25main/addons/Addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 5 additions & 1 deletion libs/s25main/addons/const_addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
//
// Add the #include for your AddonXXX.h in Addons.h!
//
// TODO(Replay) TODO(Savegame): Remove INEXHAUSTIBLE_MINES once the gamedata version can be raised and legacy
// settings/savegames no longer need migration.
ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x00000001, REFUND_MATERIALS = 0x00000002,
EXHAUSTIBLE_WATER = 0x00000003, REFUND_ON_EMERGENCY = 0x00000004, MANUAL_ROAD_ENLARGEMENT = 0x00000005,
CATAPULT_GRAPHICS = 0x00000006, METALWORKSBEHAVIORONZERO = 0x00000007,
Expand All @@ -57,7 +59,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,
Expand Down
37 changes: 37 additions & 0 deletions libs/s25main/ai/AIInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <limits>
#include <numeric>
Expand Down Expand Up @@ -45,6 +46,14 @@ bool IsPointOK_RoadPathEvenStep(const GameWorldBase& gwb, const MapPoint pt, con
const auto* prp = static_cast<const Param_RoadPath*>(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<unsigned>(resource.getAmount()), defaultRating));
}
} // namespace

AIInterface::AIInterface(const GameWorldBase& gwb, std::vector<gc::GameCommandPtr>& gcs, unsigned char playerID)
Expand Down Expand Up @@ -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];
Expand All @@ -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> direction,
int lastval) const
{
Expand Down
2 changes: 2 additions & 0 deletions libs/s25main/ai/AIInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading