From c7474f18d6881f0d3b563677a8bf304f2c7d460b Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Mon, 15 Jun 2026 19:07:51 +0200 Subject: [PATCH 1/6] Hunter: Use GetPointsInRadius() instead of square --- libs/s25main/ai/aijh/AIPlayerJH.cpp | 61 +++++++++-------------------- libs/s25main/figures/nofHunter.cpp | 47 ++++++++-------------- libs/s25main/figures/nofSkinner.cpp | 2 +- libs/s25main/gameData/GameConsts.h | 3 ++ 4 files changed, 38 insertions(+), 75 deletions(-) diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index 6a665bf2a2..02c5e3131c 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -1965,51 +1965,26 @@ bool AIPlayerJH::HuntablesinRange(const MapPoint pt, unsigned min) // check first if no other hunter(or hunter buildingsite) is nearby if(aii.isBuildingNearby(BuildingType::Hunter, pt, 14)) return false; - unsigned maxrange = 25; - unsigned short fx, fy, lx, ly; - const unsigned short SQUARE_SIZE = 19; - unsigned huntablecount = 0; - if(pt.x > SQUARE_SIZE) - fx = pt.x - SQUARE_SIZE; - else - fx = 0; - if(pt.y > SQUARE_SIZE) - fy = pt.y - SQUARE_SIZE; - else - fy = 0; - if(pt.x + SQUARE_SIZE < gwb.GetWidth()) - lx = pt.x + SQUARE_SIZE; - else - lx = gwb.GetWidth() - 1; - if(pt.y + SQUARE_SIZE < gwb.GetHeight()) - ly = pt.y + SQUARE_SIZE; - else - ly = gwb.GetHeight() - 1; - // Durchgehen und nach Tieren suchen - for(MapPoint p2(0, fy); p2.y <= ly; ++p2.y) - { - for(p2.x = fx; p2.x <= lx; ++p2.x) + const unsigned maxrange = 25; + + const auto pointToAnimal = [this](const MapPoint pt, unsigned) -> const noAnimal* { + for(const noBase& fig : gwb.GetFigures(pt)) { - // Search for animals - for(const noBase& fig : gwb.GetFigures(p2)) - { - if(fig.GetType() == NodalObjectType::Animal) - { - // Ist das Tier überhaupt zum Jagen geeignet? - if(!static_cast(fig).CanHunted()) - continue; - // Und komme ich hin? - if(gwb.FindHumanPath(pt, static_cast(fig).GetPos(), maxrange)) - // Dann nehmen wir es - { - if(++huntablecount >= min) - return true; - } - } - } + if(fig.GetType() == NodalObjectType::Animal) + return static_cast(&fig); } - } - return false; + return nullptr; + }; + + const auto canAnimalBeHunted = [this, pt, maxrange](const noAnimal* const animal) { + return animal && animal->CanHunted() + && gwb.FindHumanPath(pt, animal->GetPos(), maxrange); + }; + + const auto available_animals = + gwb.GetPointsInRadius(pt, ANIMAL_RADIUS, pointToAnimal, canAnimalBeHunted, true); + + return available_animals.size() >= min; } void AIPlayerJH::InitStoreAndMilitarylists() diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index c22745b8fb..26b5f9e921 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -131,39 +131,24 @@ void nofHunter::HandleDerivedEvent(unsigned /*id*/) void nofHunter::TryStartHunting() { - // Find animals in a square around building (actually should be circle, but animals are moving anyway) - const int SQUARE_SIZE = 19; - - // Liste mit den gefundenen Tieren - std::vector available_animals; - - // Durchgehen und nach Tieren suchen - Position curPos; - for(curPos.y = pos.y - SQUARE_SIZE; curPos.y <= pos.y + SQUARE_SIZE; ++curPos.y) - { - for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x) + // Find animals in a circle around building + const auto pointToAnimal = [world = this->world](const MapPoint pt, unsigned) -> noAnimal* { + for(auto& figure : world->GetFigures(pt)) { - MapPoint curMapPos = world->MakeMapPoint(curPos); - - // nach Tieren suchen - for(auto& figure : world->GetFigures(curMapPos)) - { - if(figure.GetType() != NodalObjectType::Animal) - continue; - // Ist das Tier überhaupt zum Jagen geeignet? - auto& animal = static_cast(figure); - if(!animal.CanHunted()) - continue; - - // Und komme ich hin? - if(pos == animal.GetPos() || world->FindHumanPath(pos, animal.GetPos(), MAX_HUNTING_DISTANCE)) - { - // Dann nehmen wir es - available_animals.push_back(&animal); - } - } + if(figure.GetType() != NodalObjectType::Animal) + continue; + return static_cast(&figure); } - } + return nullptr; + }; + + const auto canAnimalBeHunted = [pos = this->pos](const noAnimal* const animal) { + return animal && animal->CanHunted() + && (pos == animal->GetPos() || world->FindHumanPath(pos, animal->GetPos(), MAX_HUNTING_DISTANCE)); + }; + + const auto available_animals = + world->GetPointsInRadius(pos, ANIMAL_RADIUS, pointToAnimal, canAnimalBeHunted, true); // Gibt es überhaupt ein Tier, das ich jagen kann? if(!available_animals.empty()) diff --git a/libs/s25main/figures/nofSkinner.cpp b/libs/s25main/figures/nofSkinner.cpp index dbb02f61ff..bcb2705410 100644 --- a/libs/s25main/figures/nofSkinner.cpp +++ b/libs/s25main/figures/nofSkinner.cpp @@ -16,6 +16,7 @@ #include "random/Random.h" #include "world/GameWorld.h" #include "nodeObjs/noAnimal.h" +#include "gameData/GameConsts.h" #include "gameData/JobConsts.h" using namespace leatheraddon; @@ -187,7 +188,6 @@ void nofSkinner::TryStartSkinning() HandleStateWaiting1(); else { - const int ANIMAL_RADIUS = 19; const auto pointToAnimal = [world = this->world](const MapPoint pt, unsigned) -> noAnimal* { for(auto& figure : world->GetFigures(pt)) { diff --git a/libs/s25main/gameData/GameConsts.h b/libs/s25main/gameData/GameConsts.h index cbc24b7920..2a17b91590 100644 --- a/libs/s25main/gameData/GameConsts.h +++ b/libs/s25main/gameData/GameConsts.h @@ -43,6 +43,9 @@ constexpr auto gfs_to_duration(const unsigned gfs) /// Reichweite der Bergarbeiter constexpr unsigned MINER_RADIUS = 2; +/// Suchradius für Tiere (Jäger, Gerber, KI) +constexpr unsigned ANIMAL_RADIUS = 19; + /// Konstante für die Pfadrichtung bei einer Schiffsverbindung constexpr unsigned char SHIP_DIR = 100; constexpr unsigned char INVALID_DIR = 0xFF; From 2a4db77e501a09007ca484f6fa198f4ca33a7be5 Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Tue, 16 Jun 2026 10:53:16 +0200 Subject: [PATCH 2/6] DRY ANIMAL_RADIUS code --- libs/s25main/ai/aijh/AIPlayerJH.cpp | 19 +++---------------- libs/s25main/figures/nofHunter.cpp | 28 ++++++++++++++++------------ libs/s25main/figures/nofHunter.h | 6 ++++++ libs/s25main/figures/nofSkinner.cpp | 20 +++----------------- libs/s25main/gameData/GameConsts.h | 2 +- 5 files changed, 29 insertions(+), 46 deletions(-) diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index 02c5e3131c..cc03c57536 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -17,6 +17,7 @@ #include "buildings/nobHarborBuilding.h" #include "buildings/nobMilitary.h" #include "buildings/nobUsual.h" +#include "figures/nofHunter.h" #include "helpers/IdRange.h" #include "helpers/MaxEnumValue.h" #include "helpers/containerUtils.h" @@ -1965,24 +1966,10 @@ bool AIPlayerJH::HuntablesinRange(const MapPoint pt, unsigned min) // check first if no other hunter(or hunter buildingsite) is nearby if(aii.isBuildingNearby(BuildingType::Hunter, pt, 14)) return false; - const unsigned maxrange = 25; - - const auto pointToAnimal = [this](const MapPoint pt, unsigned) -> const noAnimal* { - for(const noBase& fig : gwb.GetFigures(pt)) - { - if(fig.GetType() == NodalObjectType::Animal) - return static_cast(&fig); - } - return nullptr; - }; - - const auto canAnimalBeHunted = [this, pt, maxrange](const noAnimal* const animal) { - return animal && animal->CanHunted() - && gwb.FindHumanPath(pt, animal->GetPos(), maxrange); - }; + constexpr unsigned maxrange = 25; const auto available_animals = - gwb.GetPointsInRadius(pt, ANIMAL_RADIUS, pointToAnimal, canAnimalBeHunted, true); + nofHunter::GetAnimalsInRange(gwb, pt, ANIMAL_RADIUS, maxrange, [](const noAnimal* a) { return a->CanHunted(); }); return available_animals.size() >= min; } diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index 26b5f9e921..e35f6397ed 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -129,26 +129,30 @@ void nofHunter::HandleDerivedEvent(unsigned /*id*/) } } -void nofHunter::TryStartHunting() +std::vector nofHunter::GetAnimalsInRange(const GameWorldBase& world, const MapPoint pos, unsigned radius, + unsigned maxDistance, bool (*isValidAnimal)(const noAnimal*)) { - // Find animals in a circle around building - const auto pointToAnimal = [world = this->world](const MapPoint pt, unsigned) -> noAnimal* { - for(auto& figure : world->GetFigures(pt)) + const auto pointToAnimal = [&world](const MapPoint pt, unsigned) -> noAnimal* { + for(auto& figure : world.GetFigures(pt)) { - if(figure.GetType() != NodalObjectType::Animal) - continue; - return static_cast(&figure); + if(figure.GetType() == NodalObjectType::Animal) + return static_cast(&figure); } return nullptr; }; - const auto canAnimalBeHunted = [pos = this->pos](const noAnimal* const animal) { - return animal && animal->CanHunted() - && (pos == animal->GetPos() || world->FindHumanPath(pos, animal->GetPos(), MAX_HUNTING_DISTANCE)); + const auto canAnimalBeUsed = [pos, maxDistance, &world, isValidAnimal](const noAnimal* const animal) { + return animal && isValidAnimal(animal) + && (pos == animal->GetPos() || world.FindHumanPath(pos, animal->GetPos(), maxDistance)); }; - const auto available_animals = - world->GetPointsInRadius(pos, ANIMAL_RADIUS, pointToAnimal, canAnimalBeHunted, true); + return world.GetPointsInRadius(pos, radius, pointToAnimal, canAnimalBeUsed, true); +} + +void nofHunter::TryStartHunting() +{ + const auto available_animals = GetAnimalsInRange(*world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, + [](const noAnimal* a) { return a->CanHunted(); }); // Gibt es überhaupt ein Tier, das ich jagen kann? if(!available_animals.empty()) diff --git a/libs/s25main/figures/nofHunter.h b/libs/s25main/figures/nofHunter.h index 1f780ebd5d..3070db5a6e 100644 --- a/libs/s25main/figures/nofHunter.h +++ b/libs/s25main/figures/nofHunter.h @@ -6,7 +6,9 @@ #include "nofBuildingWorker.h" #include "gameTypes/Direction.h" +#include +class GameWorldBase; class noAnimal; class SerializedGameData; class nobUsual; @@ -64,6 +66,10 @@ class nofHunter : public nofBuildingWorker void TryStartHunting(); + /// Find animals in a radius around a position that satisfy a predicate and are reachable + static std::vector GetAnimalsInRange(const GameWorldBase& world, MapPoint pos, unsigned radius, + unsigned maxDistance, bool (*isValidAnimal)(const noAnimal*)); + /// das Tier ist nicht mehr verfügbar (von selbst gestorben o.Ä.) void AnimalLost(); /// wird aufgerufen, wenn die Arbeit abgebrochen wird (von nofBuildingWorker aufgerufen) diff --git a/libs/s25main/figures/nofSkinner.cpp b/libs/s25main/figures/nofSkinner.cpp index bcb2705410..13769c3bac 100644 --- a/libs/s25main/figures/nofSkinner.cpp +++ b/libs/s25main/figures/nofSkinner.cpp @@ -10,6 +10,7 @@ #include "SerializedGameData.h" #include "SoundManager.h" #include "buildings/nobUsual.h" +#include "figures/nofHunter.h" #include "network/GameClient.h" #include "notifications/BuildingNote.h" #include "ogl/glArchivItem_Bitmap_Player.h" @@ -188,23 +189,8 @@ void nofSkinner::TryStartSkinning() HandleStateWaiting1(); else { - const auto pointToAnimal = [world = this->world](const MapPoint pt, unsigned) -> noAnimal* { - for(auto& figure : world->GetFigures(pt)) - { - if(figure.GetType() != NodalObjectType::Animal) - continue; - return checkedCast(&figure); - } - return nullptr; - }; - - const auto canAnimalBeSkinned = [pos = this->pos](const noAnimal* const animal) { - return animal && animal->CanBeSkinned() - && (pos == animal->GetPos() || world->FindHumanPath(pos, animal->GetPos(), MAX_SKINNING_DISTANCE)); - }; - - const auto available_animals = - world->GetPointsInRadius(pos, ANIMAL_RADIUS, pointToAnimal, canAnimalBeSkinned, true); + const auto available_animals = nofHunter::GetAnimalsInRange( + *world, pos, ANIMAL_RADIUS, MAX_SKINNING_DISTANCE, [](const noAnimal* a) { return a->CanBeSkinned(); }); if(!available_animals.empty()) { diff --git a/libs/s25main/gameData/GameConsts.h b/libs/s25main/gameData/GameConsts.h index 2a17b91590..b76a46d4a0 100644 --- a/libs/s25main/gameData/GameConsts.h +++ b/libs/s25main/gameData/GameConsts.h @@ -43,7 +43,7 @@ constexpr auto gfs_to_duration(const unsigned gfs) /// Reichweite der Bergarbeiter constexpr unsigned MINER_RADIUS = 2; -/// Suchradius für Tiere (Jäger, Gerber, KI) +/// Search radius for animals (hunter, skinner, AI) constexpr unsigned ANIMAL_RADIUS = 19; /// Konstante für die Pfadrichtung bei einer Schiffsverbindung From 8e7afb1993648e3601ead2b9d54a1779c768972d Mon Sep 17 00:00:00 2001 From: Morgan Christiansson Date: Thu, 18 Jun 2026 20:46:57 +0200 Subject: [PATCH 3/6] Replay compatiblity --- libs/s25main/Replay.cpp | 3 +- libs/s25main/ai/aijh/AIPlayerJH.cpp | 51 +++++++++++++++++++++++++++-- libs/s25main/figures/nofHunter.cpp | 33 +++++++++++++++++-- libs/s25main/network/GameClient.cpp | 2 ++ libs/s25main/world/GameWorldBase.h | 7 ++++ tests/s25Main/autoplay/main.cpp | 1 + 6 files changed, 91 insertions(+), 6 deletions(-) diff --git a/libs/s25main/Replay.cpp b/libs/s25main/Replay.cpp index d37c9d5cfe..f5ea3054a2 100644 --- a/libs/s25main/Replay.cpp +++ b/libs/s25main/Replay.cpp @@ -41,7 +41,8 @@ uint8_t Replay::GetLatestMinorVersion() const // 8.2: Set correct initial distributions if replay starts without savegame for leather addon (see GameClient.cpp // StartReplay function for detailed description) // 8.3 Remove invalid fish for replays started from start (i.e. map instead of savegame) - return 3; + // 8.4 Hunter/skinner/AI search for animals in a circle instead of a square (radius-based) + return 4; } uint8_t Replay::GetLatestMajorVersion() const diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index cc03c57536..e531125be0 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -1968,10 +1968,55 @@ bool AIPlayerJH::HuntablesinRange(const MapPoint pt, unsigned min) return false; constexpr unsigned maxrange = 25; - const auto available_animals = - nofHunter::GetAnimalsInRange(gwb, pt, ANIMAL_RADIUS, maxrange, [](const noAnimal* a) { return a->CanHunted(); }); + if(gwb.GetReplayMinorVersion() >= 4) + { + const auto available_animals = + nofHunter::GetAnimalsInRange(gwb, pt, ANIMAL_RADIUS, maxrange, [](const noAnimal* a) { return a->CanHunted(); }); - return available_animals.size() >= min; + return available_animals.size() >= min; + } else + { + // Legacy square search for replays recorded with old code + unsigned short fx, fy, lx, ly; + const unsigned short SQUARE_SIZE = 19; + unsigned huntablecount = 0; + if(pt.x > SQUARE_SIZE) + fx = pt.x - SQUARE_SIZE; + else + fx = 0; + if(pt.y > SQUARE_SIZE) + fy = pt.y - SQUARE_SIZE; + else + fy = 0; + if(pt.x + SQUARE_SIZE < gwb.GetWidth()) + lx = pt.x + SQUARE_SIZE; + else + lx = gwb.GetWidth() - 1; + if(pt.y + SQUARE_SIZE < gwb.GetHeight()) + ly = pt.y + SQUARE_SIZE; + else + ly = gwb.GetHeight() - 1; + for(MapPoint p2(0, fy); p2.y <= ly; ++p2.y) + { + for(p2.x = fx; p2.x <= lx; ++p2.x) + { + for(const noBase& fig : gwb.GetFigures(p2)) + { + if(fig.GetType() == NodalObjectType::Animal) + { + if(!static_cast(fig).CanHunted()) + continue; + if(gwb.FindHumanPath(pt, static_cast(fig).GetPos(), maxrange)) + { + if(++huntablecount >= min) + return true; + } + } + } + } + } + return false; + } } void AIPlayerJH::InitStoreAndMilitarylists() diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index e35f6397ed..917abc7074 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -151,8 +151,37 @@ std::vector nofHunter::GetAnimalsInRange(const GameWorldBase& world, void nofHunter::TryStartHunting() { - const auto available_animals = GetAnimalsInRange(*world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, - [](const noAnimal* a) { return a->CanHunted(); }); + std::vector available_animals; + + if(world->GetReplayMinorVersion() >= 4) + { + available_animals = GetAnimalsInRange(*world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, + [](const noAnimal* a) { return a->CanHunted(); }); + } else + { + // Legacy square search for replays recorded with old code + const int SQUARE_SIZE = 19; + Position curPos; + for(curPos.y = pos.y - SQUARE_SIZE; curPos.y <= pos.y + SQUARE_SIZE; ++curPos.y) + { + for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x) + { + MapPoint curMapPos = world->MakeMapPoint(curPos); + for(auto& figure : world->GetFigures(curMapPos)) + { + if(figure.GetType() != NodalObjectType::Animal) + continue; + auto& animal = static_cast(figure); + if(!animal.CanHunted()) + continue; + if(pos == animal.GetPos() || world->FindHumanPath(pos, animal.GetPos(), MAX_HUNTING_DISTANCE)) + { + available_animals.push_back(&animal); + } + } + } + } + } // Gibt es überhaupt ein Tier, das ich jagen kann? if(!available_animals.empty()) diff --git a/libs/s25main/network/GameClient.cpp b/libs/s25main/network/GameClient.cpp index e7abe4f324..24cbc25499 100644 --- a/libs/s25main/network/GameClient.cpp +++ b/libs/s25main/network/GameClient.cpp @@ -343,6 +343,8 @@ void GameClient::StartGame(const unsigned random_init) const bool fixFish = !GetReplay() || GetReplay()->GetMinorVersion() >= 3; MapLoader::SetupResources(gameWorld, fixFish); } + if(replayMode && replayinfo) + gameWorld.SetReplayMinorVersion(replayinfo->replay.GetMinorVersion()); gameWorld.InitAfterLoad(); // Update visual settings diff --git a/libs/s25main/world/GameWorldBase.h b/libs/s25main/world/GameWorldBase.h index 1a4e1f0265..c1d3e396e7 100644 --- a/libs/s25main/world/GameWorldBase.h +++ b/libs/s25main/world/GameWorldBase.h @@ -68,6 +68,8 @@ class GameWorldBase : public World /// Interface zum GUI GameInterface* gi; std::unique_ptr econHandler; + /// Replay minor version when replaying. Set to current minor version for live games. + uint8_t replayMinorVersion_ = 255; public: GameWorldBase(std::vector players, const GlobalGameSettings& gameSettings, EventManager& em); @@ -164,6 +166,11 @@ class GameWorldBase : public World bool IsSinglePlayer() const; /// Return the game settings const GlobalGameSettings& GetGGS() const { return gameSettings; } + /// Get the replay minor version (used for backward compatibility) + uint8_t GetReplayMinorVersion() const { return replayMinorVersion_; } + /// Set the replay minor version (called when loading a replay) + void SetReplayMinorVersion(uint8_t version) { replayMinorVersion_ = version; } + EventManager& GetEvMgr() { return em; } const EventManager& GetEvMgr() const { return em; } SoundManager& GetSoundMgr() { return *soundManager; } diff --git a/tests/s25Main/autoplay/main.cpp b/tests/s25Main/autoplay/main.cpp index 1558a7a9db..32c93d15c7 100644 --- a/tests/s25Main/autoplay/main.cpp +++ b/tests/s25Main/autoplay/main.cpp @@ -92,6 +92,7 @@ static void playReplay(const boost::filesystem::path& replayPath, const bool isS player.MakeStartPacts(); } + gameWorld.SetReplayMinorVersion(replay.GetMinorVersion()); gameWorld.InitAfterLoad(); bool endOfReplay = false; From 53b3422b49ebf3d4e24560cb1b61f88fd72b4e77 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 14:21:10 +0200 Subject: [PATCH 4/6] Rename `ReplayMinorVersion` to `ReplayCompatVersion` --- libs/s25main/ai/aijh/AIPlayerJH.cpp | 2 +- libs/s25main/figures/nofHunter.cpp | 2 +- libs/s25main/network/GameClient.cpp | 2 +- libs/s25main/world/GameWorldBase.h | 13 +++++++------ tests/s25Main/autoplay/main.cpp | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index e531125be0..44323b74c4 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -1968,7 +1968,7 @@ bool AIPlayerJH::HuntablesinRange(const MapPoint pt, unsigned min) return false; constexpr unsigned maxrange = 25; - if(gwb.GetReplayMinorVersion() >= 4) + if(gwb.GetReplayCompatVersion() >= 4) { const auto available_animals = nofHunter::GetAnimalsInRange(gwb, pt, ANIMAL_RADIUS, maxrange, [](const noAnimal* a) { return a->CanHunted(); }); diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index 917abc7074..6f159518b6 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -153,7 +153,7 @@ void nofHunter::TryStartHunting() { std::vector available_animals; - if(world->GetReplayMinorVersion() >= 4) + if(world->GetReplayCompatVersion() >= 4) { available_animals = GetAnimalsInRange(*world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, [](const noAnimal* a) { return a->CanHunted(); }); diff --git a/libs/s25main/network/GameClient.cpp b/libs/s25main/network/GameClient.cpp index 24cbc25499..77236c2611 100644 --- a/libs/s25main/network/GameClient.cpp +++ b/libs/s25main/network/GameClient.cpp @@ -344,7 +344,7 @@ void GameClient::StartGame(const unsigned random_init) MapLoader::SetupResources(gameWorld, fixFish); } if(replayMode && replayinfo) - gameWorld.SetReplayMinorVersion(replayinfo->replay.GetMinorVersion()); + gameWorld.SetReplayCompatVersion(replayinfo->replay.GetMinorVersion()); gameWorld.InitAfterLoad(); // Update visual settings diff --git a/libs/s25main/world/GameWorldBase.h b/libs/s25main/world/GameWorldBase.h index c1d3e396e7..ca488d9e52 100644 --- a/libs/s25main/world/GameWorldBase.h +++ b/libs/s25main/world/GameWorldBase.h @@ -13,6 +13,7 @@ #include "postSystem/PostManager.h" #include "world/World.h" #include "s25util/span.hpp" +#include #include #include #include @@ -68,8 +69,8 @@ class GameWorldBase : public World /// Interface zum GUI GameInterface* gi; std::unique_ptr econHandler; - /// Replay minor version when replaying. Set to current minor version for live games. - uint8_t replayMinorVersion_ = 255; + /// Replay minor version for backwards compatibility. Default is highest possible, i.e. current behavior. + uint8_t replayCompatVersion_ = std::numeric_limits::max(); public: GameWorldBase(std::vector players, const GlobalGameSettings& gameSettings, EventManager& em); @@ -166,10 +167,10 @@ class GameWorldBase : public World bool IsSinglePlayer() const; /// Return the game settings const GlobalGameSettings& GetGGS() const { return gameSettings; } - /// Get the replay minor version (used for backward compatibility) - uint8_t GetReplayMinorVersion() const { return replayMinorVersion_; } - /// Set the replay minor version (called when loading a replay) - void SetReplayMinorVersion(uint8_t version) { replayMinorVersion_ = version; } + /// Get the replay (minor) version with which (backward) compatibility is enabled. + uint8_t GetReplayCompatVersion() const { return replayCompatVersion_; } + /// Set the replay minor version to change the behavior of the game for backward compatibility. + void SetReplayCompatVersion(uint8_t version) { replayCompatVersion_ = version; } EventManager& GetEvMgr() { return em; } const EventManager& GetEvMgr() const { return em; } diff --git a/tests/s25Main/autoplay/main.cpp b/tests/s25Main/autoplay/main.cpp index 32c93d15c7..9eed29876f 100644 --- a/tests/s25Main/autoplay/main.cpp +++ b/tests/s25Main/autoplay/main.cpp @@ -92,7 +92,7 @@ static void playReplay(const boost::filesystem::path& replayPath, const bool isS player.MakeStartPacts(); } - gameWorld.SetReplayMinorVersion(replay.GetMinorVersion()); + gameWorld.SetReplayCompatVersion(replay.GetMinorVersion()); gameWorld.InitAfterLoad(); bool endOfReplay = false; From cf83a024dbaeb48cafdcb3d6fd401293a6ae3dbe Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 14:24:12 +0200 Subject: [PATCH 5/6] Add `nofHunter::GetHuntableAnimalsInRange` --- libs/s25main/figures/nofHunter.cpp | 12 ++++++++---- libs/s25main/figures/nofHunter.h | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index 6f159518b6..8a74709652 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -149,15 +149,19 @@ std::vector nofHunter::GetAnimalsInRange(const GameWorldBase& world, return world.GetPointsInRadius(pos, radius, pointToAnimal, canAnimalBeUsed, true); } +std::vector nofHunter::GetHuntableAnimalsInRange(const GameWorldBase& world, MapPoint pos) +{ + return GetAnimalsInRange(world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, + [](const noAnimal* a) { return a->CanHunted(); }); +} + void nofHunter::TryStartHunting() { std::vector available_animals; if(world->GetReplayCompatVersion() >= 4) - { - available_animals = GetAnimalsInRange(*world, pos, ANIMAL_RADIUS, MAX_HUNTING_DISTANCE, - [](const noAnimal* a) { return a->CanHunted(); }); - } else + available_animals = GetHuntableAnimalsInRange(*world, pos); + else { // Legacy square search for replays recorded with old code const int SQUARE_SIZE = 19; diff --git a/libs/s25main/figures/nofHunter.h b/libs/s25main/figures/nofHunter.h index 3070db5a6e..94e6a9ee05 100644 --- a/libs/s25main/figures/nofHunter.h +++ b/libs/s25main/figures/nofHunter.h @@ -69,6 +69,7 @@ class nofHunter : public nofBuildingWorker /// Find animals in a radius around a position that satisfy a predicate and are reachable static std::vector GetAnimalsInRange(const GameWorldBase& world, MapPoint pos, unsigned radius, unsigned maxDistance, bool (*isValidAnimal)(const noAnimal*)); + static std::vector GetHuntableAnimalsInRange(const GameWorldBase& world, MapPoint pos); /// das Tier ist nicht mehr verfügbar (von selbst gestorben o.Ä.) void AnimalLost(); From d8277bc8397be253ecef5af4f8214229a4934cd3 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Sun, 4 Oct 2026 14:24:38 +0200 Subject: [PATCH 6/6] Remove replay compat from AI --- libs/s25main/ai/aijh/AIPlayerJH.cpp | 58 ++--------------------------- libs/s25main/ai/aijh/AIPlayerJH.h | 2 +- 2 files changed, 5 insertions(+), 55 deletions(-) diff --git a/libs/s25main/ai/aijh/AIPlayerJH.cpp b/libs/s25main/ai/aijh/AIPlayerJH.cpp index 44323b74c4..c09fbe8381 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.cpp +++ b/libs/s25main/ai/aijh/AIPlayerJH.cpp @@ -939,7 +939,7 @@ MapPoint AIPlayerJH::FindPositionForBuildingAround(BuildingType type, const MapP case BuildingType::Hunter: { // check if there are any animals in range - if(HuntablesinRange(around, (2 << GetBldPlanner().GetNumBuildings(BuildingType::Hunter)))) + if(HuntablesInRange(around, (2 << GetBldPlanner().GetNumBuildings(BuildingType::Hunter)))) foundPos = SimpleFindPosition(around, BUILDING_SIZE[type], searchRadius); break; } @@ -1961,62 +1961,12 @@ unsigned AIPlayerJH::SoldierAvailable(int rank) return freeSoldiers; } -bool AIPlayerJH::HuntablesinRange(const MapPoint pt, unsigned min) +bool AIPlayerJH::HuntablesInRange(const MapPoint pt, unsigned min) { - // check first if no other hunter(or hunter buildingsite) is nearby + // check first if no other hunter (or hunter buildingsite) is nearby if(aii.isBuildingNearby(BuildingType::Hunter, pt, 14)) return false; - constexpr unsigned maxrange = 25; - - if(gwb.GetReplayCompatVersion() >= 4) - { - const auto available_animals = - nofHunter::GetAnimalsInRange(gwb, pt, ANIMAL_RADIUS, maxrange, [](const noAnimal* a) { return a->CanHunted(); }); - - return available_animals.size() >= min; - } else - { - // Legacy square search for replays recorded with old code - unsigned short fx, fy, lx, ly; - const unsigned short SQUARE_SIZE = 19; - unsigned huntablecount = 0; - if(pt.x > SQUARE_SIZE) - fx = pt.x - SQUARE_SIZE; - else - fx = 0; - if(pt.y > SQUARE_SIZE) - fy = pt.y - SQUARE_SIZE; - else - fy = 0; - if(pt.x + SQUARE_SIZE < gwb.GetWidth()) - lx = pt.x + SQUARE_SIZE; - else - lx = gwb.GetWidth() - 1; - if(pt.y + SQUARE_SIZE < gwb.GetHeight()) - ly = pt.y + SQUARE_SIZE; - else - ly = gwb.GetHeight() - 1; - for(MapPoint p2(0, fy); p2.y <= ly; ++p2.y) - { - for(p2.x = fx; p2.x <= lx; ++p2.x) - { - for(const noBase& fig : gwb.GetFigures(p2)) - { - if(fig.GetType() == NodalObjectType::Animal) - { - if(!static_cast(fig).CanHunted()) - continue; - if(gwb.FindHumanPath(pt, static_cast(fig).GetPos(), maxrange)) - { - if(++huntablecount >= min) - return true; - } - } - } - } - } - return false; - } + return nofHunter::GetHuntableAnimalsInRange(gwb, pt).size() >= min; } void AIPlayerJH::InitStoreAndMilitarylists() diff --git a/libs/s25main/ai/aijh/AIPlayerJH.h b/libs/s25main/ai/aijh/AIPlayerJH.h index 0763e9baf7..b1d461bb50 100644 --- a/libs/s25main/ai/aijh/AIPlayerJH.h +++ b/libs/s25main/ai/aijh/AIPlayerJH.h @@ -201,7 +201,7 @@ class AIPlayerJH final : public AIPlayer // check if there are free soldiers (in hq/storehouses) unsigned SoldierAvailable(int rank = -1); - bool HuntablesinRange(MapPoint pt, unsigned min); + bool HuntablesInRange(MapPoint pt, unsigned min); bool ValidTreeinRange(MapPoint pt);