From 3f214f71e2784c2d4adc623b158966d5cb826daf Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 1 May 2026 22:16:15 +0200 Subject: [PATCH 1/3] Keep foresters off potential farm fields --- libs/s25main/figures/nofForester.cpp | 44 ++++++++++++++++++++++++ tests/s25Main/integration/testFarmer.cpp | 23 +++++++++++++ 2 files changed, 67 insertions(+) diff --git a/libs/s25main/figures/nofForester.cpp b/libs/s25main/figures/nofForester.cpp index 0c6aa40742..eceff20a5e 100644 --- a/libs/s25main/figures/nofForester.cpp +++ b/libs/s25main/figures/nofForester.cpp @@ -8,6 +8,7 @@ #include "GamePlayer.h" #include "Loader.h" #include "SoundManager.h" +#include "buildings/noBaseBuilding.h" #include "network/GameClient.h" #include "ogl/glArchivItem_Bitmap_Player.h" #include "random/Random.h" @@ -15,6 +16,45 @@ #include "nodeObjs/noTree.h" #include +namespace { +bool IsPotentialNewFieldForOwnFarm(GameWorld& world, const MapPoint pt, const unsigned char player) +{ + constexpr unsigned FARM_FIELD_RADIUS = 2; + + for(const auto dir : helpers::EnumRange{}) + { + if(world.GetPointRoad(pt, dir) != PointRoad::None) + return false; + } + + if(!world.IsOfTerrain(pt, [](const auto& desc) { return desc.IsVital(); })) + return false; + + const NodalObjectType noType = world.GetNO(pt)->GetType(); + if(noType != NodalObjectType::Environment && noType != NodalObjectType::Nothing) + return false; + + for(const MapPoint nb : world.GetNeighbours(pt)) + { + const NodalObjectType nbType = world.GetNO(nb)->GetType(); + if(nbType == NodalObjectType::Grainfield || nbType == NodalObjectType::Grapefield + || nbType == NodalObjectType::Building || nbType == NodalObjectType::Buildingsite) + return false; + } + + return world.CheckPointsInRadius( + pt, FARM_FIELD_RADIUS, + [&world, player](const MapPoint farmPt, unsigned) { + if(world.GetNO(farmPt)->GetType() != NodalObjectType::Building) + return false; + + const auto* building = world.GetSpecObj(farmPt); + return building && building->GetPlayer() == player && building->GetBuildingType() == BuildingType::Farm; + }, + false); +} +} // namespace + nofForester::nofForester(const MapPoint pos, const unsigned char player, nobUsual* workplace) : nofFarmhand(Job::Forester, pos, player, workplace) {} @@ -110,6 +150,10 @@ nofFarmhand::PointQuality nofForester::GetPointQuality(const MapPoint pt, bool / return PointQuality::NotPossible; } + // Avoid occupying spots that an own farm could use for a new grain field. + if(IsPotentialNewFieldForOwnFarm(*world, pt, player)) + return PointQuality::NotPossible; + // Terrain untersuchen if(world->IsOfTerrain(pt, [](const auto& desc) { return desc.IsVital(); })) return PointQuality::Class1; diff --git a/tests/s25Main/integration/testFarmer.cpp b/tests/s25Main/integration/testFarmer.cpp index 3a79e00500..6290b291b8 100644 --- a/tests/s25Main/integration/testFarmer.cpp +++ b/tests/s25Main/integration/testFarmer.cpp @@ -6,6 +6,7 @@ #include "buildings/nobUsual.h" #include "factories/BuildingFactory.h" #include "figures/nofFarmhand.h" +#include "figures/nofForester.h" #include "worldFixtures/CreateEmptyWorld.h" #include "worldFixtures/WorldFixture.h" #include "worldFixtures/initGameRNG.hpp" @@ -33,6 +34,28 @@ struct FarmerFixture : public WorldFixture } }; +BOOST_FIXTURE_TEST_CASE(ForesterAvoidsPotentialFarmFieldSpots, FarmerFixture) +{ + initGameRNG(); + + const auto isPointAvailable = [](const nofFarmhand* worker, const MapPoint pt) { + return worker->GetPointQuality(pt) != nofFarmhand::PointQuality::NotPossible; + }; + + const MapPoint fieldPt = world.GetNeighbour2(farmPt, 0); + BOOST_TEST_REQUIRE(isPointAvailable(farmer, fieldPt)); + + const MapPoint foresterPt = world.MakeMapPoint(world.GetPlayer(0).GetHQPos() - Position(5, 0)); + auto* foresterBuilding = dynamic_cast( + BuildingFactory::CreateBuilding(world, BuildingType::Forester, foresterPt, 0, Nation::Romans)); + BOOST_TEST_REQUIRE(foresterBuilding); + + nofForester foresterWorker(world.GetNeighbour(foresterPt, Direction::SouthEast), 0, foresterBuilding); + const nofFarmhand* forester = &foresterWorker; + + BOOST_TEST_REQUIRE(!isPointAvailable(forester, fieldPt)); +} + BOOST_FIXTURE_TEST_CASE(FarmFieldPlanting, FarmerFixture) { initGameRNG(); From e11591d14b26c9a4ca0391592097adc982674e33 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 2 May 2026 21:19:23 +0200 Subject: [PATCH 2/3] gameplay: gate forester farm-field avoidance behind addon --- libs/s25main/GlobalGameSettings.cpp | 3 ++- .../addons/AddonForesterFarmFieldAvoidance.h | 18 ++++++++++++++++++ libs/s25main/addons/Addons.h | 1 + libs/s25main/addons/const_addons.h | 5 ++++- libs/s25main/figures/nofForester.cpp | 6 ++++-- tests/s25Main/integration/testFarmer.cpp | 3 +++ 6 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 libs/s25main/addons/AddonForesterFarmFieldAvoidance.h diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 63f240221e..5425aef5f9 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -104,7 +104,8 @@ void GlobalGameSettings::registerAllAddons() AddonWine, AddonLeather, AddonNoArmorDefault, - AddonArmorCapturedBld + AddonArmorCapturedBld, + AddonForesterFarmFieldAvoidance >; // clang-format on using namespace boost::mp11; diff --git a/libs/s25main/addons/AddonForesterFarmFieldAvoidance.h b/libs/s25main/addons/AddonForesterFarmFieldAvoidance.h new file mode 100644 index 0000000000..f658b8193b --- /dev/null +++ b/libs/s25main/addons/AddonForesterFarmFieldAvoidance.h @@ -0,0 +1,18 @@ +// 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 "mygettext/mygettext.h" + +class AddonForesterFarmFieldAvoidance : public AddonBool +{ +public: + AddonForesterFarmFieldAvoidance() + : AddonBool(AddonId::FORESTER_FARM_FIELD_AVOIDANCE, AddonGroup::GamePlay | AddonGroup::Economy, + _("Foresters avoid farm field spots"), + _("Prevents foresters from planting trees on spots that own farms could use for new fields.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 4b773587d0..127ca6892f 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -62,6 +62,7 @@ #include "addons/AddonAutoFlags.h" #include "addons/AddonArmorCapturedBld.h" +#include "addons/AddonForesterFarmFieldAvoidance.h" #include "addons/AddonLeather.h" #include "addons/AddonNoArmorDefault.h" #include "addons/AddonWine.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 4ae5da8c88..2630f9ad44 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -26,6 +26,7 @@ // 00E Jonathan // 00F Jarno // 010 aztimh +// 011 DevOpsOfChaos // Do not forget to add your Addon to GlobalGameSettings::registerAllAddons @ GlobalGameSettings.cpp! // Never use a number twice! @@ -75,7 +76,9 @@ ENUM_WITH_STRING(AddonId, LIMIT_CATAPULTS = 0x00000000, INEXHAUSTIBLE_MINES = 0x AUTOFLAGS = 0x00F00000, WINE = 0x01000000, LEATHER = 0x01000001, NO_ARMOR_DEFAULT = 0x01000002, - ARMOR_CAPTURED_BLD = 0x01000003) + ARMOR_CAPTURED_BLD = 0x01000003, + + FORESTER_FARM_FIELD_AVOIDANCE = 0x01100000) //-V:AddonId:801 enum class AddonGroup : unsigned diff --git a/libs/s25main/figures/nofForester.cpp b/libs/s25main/figures/nofForester.cpp index eceff20a5e..a5cf30ba7a 100644 --- a/libs/s25main/figures/nofForester.cpp +++ b/libs/s25main/figures/nofForester.cpp @@ -6,8 +6,10 @@ #include "GameInterface.h" #include "GamePlayer.h" +#include "GlobalGameSettings.h" #include "Loader.h" #include "SoundManager.h" +#include "addons/const_addons.h" #include "buildings/noBaseBuilding.h" #include "network/GameClient.h" #include "ogl/glArchivItem_Bitmap_Player.h" @@ -150,8 +152,8 @@ nofFarmhand::PointQuality nofForester::GetPointQuality(const MapPoint pt, bool / return PointQuality::NotPossible; } - // Avoid occupying spots that an own farm could use for a new grain field. - if(IsPotentialNewFieldForOwnFarm(*world, pt, player)) + if(world->GetGGS().isEnabled(AddonId::FORESTER_FARM_FIELD_AVOIDANCE) + && IsPotentialNewFieldForOwnFarm(*world, pt, player)) return PointQuality::NotPossible; // Terrain untersuchen diff --git a/tests/s25Main/integration/testFarmer.cpp b/tests/s25Main/integration/testFarmer.cpp index 6290b291b8..e732f195a7 100644 --- a/tests/s25Main/integration/testFarmer.cpp +++ b/tests/s25Main/integration/testFarmer.cpp @@ -53,6 +53,9 @@ BOOST_FIXTURE_TEST_CASE(ForesterAvoidsPotentialFarmFieldSpots, FarmerFixture) nofForester foresterWorker(world.GetNeighbour(foresterPt, Direction::SouthEast), 0, foresterBuilding); const nofFarmhand* forester = &foresterWorker; + BOOST_TEST_REQUIRE(isPointAvailable(forester, fieldPt)); + + ggs.setSelection(AddonId::FORESTER_FARM_FIELD_AVOIDANCE, 1); BOOST_TEST_REQUIRE(!isPointAvailable(forester, fieldPt)); } From 13c1d17122f59a3b1f37ca4a7029e4f614b6bd77 Mon Sep 17 00:00:00 2001 From: Manu Date: Sun, 3 May 2026 11:50:14 +0200 Subject: [PATCH 3/3] Refactor farm field placement checks --- libs/s25main/figures/nofFarmer.cpp | 59 +++++++++++++----------- libs/s25main/figures/nofFarmer.h | 3 ++ libs/s25main/figures/nofFarmhand.cpp | 33 +++++++------ libs/s25main/figures/nofFarmhand.h | 2 + libs/s25main/figures/nofForester.cpp | 25 ++-------- tests/s25Main/integration/testFarmer.cpp | 11 ++--- 6 files changed, 62 insertions(+), 71 deletions(-) diff --git a/libs/s25main/figures/nofFarmer.cpp b/libs/s25main/figures/nofFarmer.cpp index 4fbb2a47a2..7a6401ea9e 100644 --- a/libs/s25main/figures/nofFarmer.cpp +++ b/libs/s25main/figures/nofFarmer.cpp @@ -57,6 +57,36 @@ unsigned short nofFarmer::GetCarryID() const return 71; } +nofFarmhand::PointQuality nofFarmer::GetNewFieldPointQuality(const GameWorld& world, const MapPoint pt) +{ + // Nicht auf Straßen bauen! + for(const auto dir : helpers::EnumRange{}) + { + if(world.GetPointRoad(pt, dir) != PointRoad::None) + return PointQuality::NotPossible; + } + + // Terrain untersuchen + if(!world.IsOfTerrain(pt, [](const auto& desc) { return desc.IsVital(); })) + return PointQuality::NotPossible; + + // Ist Platz frei? + NodalObjectType noType = world.GetNO(pt)->GetType(); + if(noType != NodalObjectType::Environment && noType != NodalObjectType::Nothing) + return PointQuality::NotPossible; + + for(const MapPoint nb : world.GetNeighbours(pt)) + { + // Nicht direkt neben andere Getreidefelder und Gebäude setzen! + noType = world.GetNO(nb)->GetType(); + if(noType == NodalObjectType::Grainfield || noType == NodalObjectType::Grapefield + || noType == NodalObjectType::Building || noType == NodalObjectType::Buildingsite) + return PointQuality::NotPossible; + } + + return PointQuality::Class2; +} + /// Abgeleitete Klasse informieren, wenn sie anfängt zu arbeiten (Vorbereitungen) void nofFarmer::WorkStarted() { @@ -123,34 +153,7 @@ nofFarmhand::PointQuality nofFarmer::GetPointQuality(const MapPoint pt, bool /* } // oder einen freien Platz, wo wir ein neues sähen können else - { - // Nicht auf Straßen bauen! - for(const auto dir : helpers::EnumRange{}) - { - if(world->GetPointRoad(pt, dir) != PointRoad::None) - return PointQuality::NotPossible; - } - - // Terrain untersuchen - if(!world->IsOfTerrain(pt, [](const auto& desc) { return desc.IsVital(); })) - return PointQuality::NotPossible; - - // Ist Platz frei? - NodalObjectType noType = world->GetNO(pt)->GetType(); - if(noType != NodalObjectType::Environment && noType != NodalObjectType::Nothing) - return PointQuality::NotPossible; - - for(const MapPoint nb : world->GetNeighbours(pt)) - { - // Nicht direkt neben andere Getreidefelder und Gebäude setzen! - noType = world->GetNO(nb)->GetType(); - if(noType == NodalObjectType::Grainfield || noType == NodalObjectType::Grapefield - || noType == NodalObjectType::Building || noType == NodalObjectType::Buildingsite) - return PointQuality::NotPossible; - } - - return PointQuality::Class2; - } + return GetNewFieldPointQuality(*world, pt); } void nofFarmer::WorkAborted() diff --git a/libs/s25main/figures/nofFarmer.h b/libs/s25main/figures/nofFarmer.h index 87934e9cec..24d3faad38 100644 --- a/libs/s25main/figures/nofFarmer.h +++ b/libs/s25main/figures/nofFarmer.h @@ -5,6 +5,7 @@ #pragma once #include "nofFarmhand.h" +class GameWorld; class SerializedGameData; class nobUsual; @@ -34,6 +35,8 @@ class nofFarmer : public nofFarmhand nofFarmer(MapPoint pos, unsigned char player, nobUsual* workplace); nofFarmer(SerializedGameData& sgd, unsigned obj_id); + static nofFarmhand::PointQuality GetNewFieldPointQuality(const GameWorld& world, MapPoint pt); + void Serialize(SerializedGameData& sgd) const override; GO_Type GetGOT() const final { return GO_Type::NofFarmer; } diff --git a/libs/s25main/figures/nofFarmhand.cpp b/libs/s25main/figures/nofFarmhand.cpp index c5230dbd75..07aed43dae 100644 --- a/libs/s25main/figures/nofFarmhand.cpp +++ b/libs/s25main/figures/nofFarmhand.cpp @@ -27,6 +27,23 @@ nofFarmhand::nofFarmhand(SerializedGameData& sgd, const unsigned obj_id) : nofBuildingWorker(sgd, obj_id), dest(sgd.PopMapPoint()) {} +unsigned nofFarmhand::GetWorkRadius(const Job job) +{ + switch(job) + { + case Job::Carpenter: return 0; + case Job::Hunter: + case Job::Farmer: + case Job::Winegrower: return 2; + case Job::CharBurner: return 3; + case Job::Woodcutter: + case Job::Forester: return 6; + case Job::Fisher: return 7; + case Job::Stonemason: return 8; + default: throw std::logic_error("Invalid job"); + } +} + void nofFarmhand::WalkedDerived() { switch(state) @@ -59,21 +76,7 @@ void nofFarmhand::HandleDerivedEvent(const unsigned /*id*/) { // Start working after the initial wait period // Work radius - const unsigned max_radius = [](Job job) { - switch(job) - { - case Job::Carpenter: return 0; - case Job::Hunter: - case Job::Farmer: - case Job::Winegrower: return 2; - case Job::CharBurner: return 3; - case Job::Woodcutter: - case Job::Forester: return 6; - case Job::Fisher: return 7; - case Job::Stonemason: return 8; - default: throw std::logic_error("Invalid job"); - } - }(job_); + const unsigned max_radius = GetWorkRadius(job_); // Number of additional radii in which points should be found // I.e. 0 => Don't search for points further away than ones already found const unsigned additionalRadiiToFind = [](Job job) { diff --git a/libs/s25main/figures/nofFarmhand.h b/libs/s25main/figures/nofFarmhand.h index 028811d0aa..910fffe8fb 100644 --- a/libs/s25main/figures/nofFarmhand.h +++ b/libs/s25main/figures/nofFarmhand.h @@ -54,6 +54,8 @@ class nofFarmhand : public nofBuildingWorker nofFarmhand(Job job, MapPoint pos, unsigned char player, nobUsual* workplace); nofFarmhand(SerializedGameData& sgd, unsigned obj_id); + static unsigned GetWorkRadius(Job job); + void Serialize(SerializedGameData& sgd) const override; void HandleDerivedEvent(unsigned id) override; diff --git a/libs/s25main/figures/nofForester.cpp b/libs/s25main/figures/nofForester.cpp index a5cf30ba7a..91b617e21e 100644 --- a/libs/s25main/figures/nofForester.cpp +++ b/libs/s25main/figures/nofForester.cpp @@ -12,6 +12,7 @@ #include "addons/const_addons.h" #include "buildings/noBaseBuilding.h" #include "network/GameClient.h" +#include "nofFarmer.h" #include "ogl/glArchivItem_Bitmap_Player.h" #include "random/Random.h" #include "world/GameWorld.h" @@ -21,31 +22,11 @@ namespace { bool IsPotentialNewFieldForOwnFarm(GameWorld& world, const MapPoint pt, const unsigned char player) { - constexpr unsigned FARM_FIELD_RADIUS = 2; - - for(const auto dir : helpers::EnumRange{}) - { - if(world.GetPointRoad(pt, dir) != PointRoad::None) - return false; - } - - if(!world.IsOfTerrain(pt, [](const auto& desc) { return desc.IsVital(); })) + if(nofFarmer::GetNewFieldPointQuality(world, pt) == nofFarmhand::PointQuality::NotPossible) return false; - const NodalObjectType noType = world.GetNO(pt)->GetType(); - if(noType != NodalObjectType::Environment && noType != NodalObjectType::Nothing) - return false; - - for(const MapPoint nb : world.GetNeighbours(pt)) - { - const NodalObjectType nbType = world.GetNO(nb)->GetType(); - if(nbType == NodalObjectType::Grainfield || nbType == NodalObjectType::Grapefield - || nbType == NodalObjectType::Building || nbType == NodalObjectType::Buildingsite) - return false; - } - return world.CheckPointsInRadius( - pt, FARM_FIELD_RADIUS, + pt, nofFarmhand::GetWorkRadius(Job::Farmer), [&world, player](const MapPoint farmPt, unsigned) { if(world.GetNO(farmPt)->GetType() != NodalObjectType::Building) return false; diff --git a/tests/s25Main/integration/testFarmer.cpp b/tests/s25Main/integration/testFarmer.cpp index e732f195a7..5b01e18952 100644 --- a/tests/s25Main/integration/testFarmer.cpp +++ b/tests/s25Main/integration/testFarmer.cpp @@ -38,12 +38,12 @@ BOOST_FIXTURE_TEST_CASE(ForesterAvoidsPotentialFarmFieldSpots, FarmerFixture) { initGameRNG(); - const auto isPointAvailable = [](const nofFarmhand* worker, const MapPoint pt) { - return worker->GetPointQuality(pt) != nofFarmhand::PointQuality::NotPossible; + const auto isPointAvailable = [](const nofFarmhand& worker, const MapPoint pt) { + return worker.GetPointQuality(pt) != nofFarmhand::PointQuality::NotPossible; }; const MapPoint fieldPt = world.GetNeighbour2(farmPt, 0); - BOOST_TEST_REQUIRE(isPointAvailable(farmer, fieldPt)); + BOOST_TEST_REQUIRE(isPointAvailable(*farmer, fieldPt)); const MapPoint foresterPt = world.MakeMapPoint(world.GetPlayer(0).GetHQPos() - Position(5, 0)); auto* foresterBuilding = dynamic_cast( @@ -51,12 +51,11 @@ BOOST_FIXTURE_TEST_CASE(ForesterAvoidsPotentialFarmFieldSpots, FarmerFixture) BOOST_TEST_REQUIRE(foresterBuilding); nofForester foresterWorker(world.GetNeighbour(foresterPt, Direction::SouthEast), 0, foresterBuilding); - const nofFarmhand* forester = &foresterWorker; - BOOST_TEST_REQUIRE(isPointAvailable(forester, fieldPt)); + BOOST_TEST_REQUIRE(isPointAvailable(foresterWorker, fieldPt)); ggs.setSelection(AddonId::FORESTER_FARM_FIELD_AVOIDANCE, 1); - BOOST_TEST_REQUIRE(!isPointAvailable(forester, fieldPt)); + BOOST_TEST_REQUIRE(!isPointAvailable(foresterWorker, fieldPt)); } BOOST_FIXTURE_TEST_CASE(FarmFieldPlanting, FarmerFixture)