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/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 0c6aa40742..91b617e21e 100644 --- a/libs/s25main/figures/nofForester.cpp +++ b/libs/s25main/figures/nofForester.cpp @@ -6,15 +6,38 @@ #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 "nofFarmer.h" #include "ogl/glArchivItem_Bitmap_Player.h" #include "random/Random.h" #include "world/GameWorld.h" #include "nodeObjs/noTree.h" #include +namespace { +bool IsPotentialNewFieldForOwnFarm(GameWorld& world, const MapPoint pt, const unsigned char player) +{ + if(nofFarmer::GetNewFieldPointQuality(world, pt) == nofFarmhand::PointQuality::NotPossible) + return false; + + return world.CheckPointsInRadius( + pt, nofFarmhand::GetWorkRadius(Job::Farmer), + [&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 +133,10 @@ nofFarmhand::PointQuality nofForester::GetPointQuality(const MapPoint pt, bool / return PointQuality::NotPossible; } + if(world->GetGGS().isEnabled(AddonId::FORESTER_FARM_FIELD_AVOIDANCE) + && 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..5b01e18952 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,30 @@ 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); + + BOOST_TEST_REQUIRE(isPointAvailable(foresterWorker, fieldPt)); + + ggs.setSelection(AddonId::FORESTER_FARM_FIELD_AVOIDANCE, 1); + BOOST_TEST_REQUIRE(!isPointAvailable(foresterWorker, fieldPt)); +} + BOOST_FIXTURE_TEST_CASE(FarmFieldPlanting, FarmerFixture) { initGameRNG();