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 72fbee111f..3fe404e2f7 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" @@ -1704,7 +1705,7 @@ void AIPlayerJH::TrySeaAttack() if(!testseaidswithattackers.empty()) { undefendedTargets.push_back(milBld); - } // else - no attackers - do nothing + } // else - no attackers - do nothing } else // normal target - check is done after random shuffle so we dont have to check every possible // target and instead only enough to get 1 good one { @@ -1965,51 +1966,57 @@ 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) + constexpr unsigned maxrange = 25; + + if(gwb.GetReplayMinorVersion() >= 4) { - for(p2.x = fx; p2.x <= lx; ++p2.x) - { - // Search for animals - for(const noBase& fig : gwb.GetFigures(p2)) + 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) { - if(fig.GetType() == NodalObjectType::Animal) + for(const noBase& fig : gwb.GetFigures(p2)) { - // 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(fig.GetType() == NodalObjectType::Animal) { - if(++huntablecount >= min) - return true; + if(!static_cast(fig).CanHunted()) + continue; + if(gwb.FindHumanPath(pt, static_cast(fig).GetPos(), maxrange)) + { + if(++huntablecount >= min) + return true; + } } } } } + return false; } - return false; } void AIPlayerJH::InitStoreAndMilitarylists() diff --git a/libs/s25main/figures/nofHunter.cpp b/libs/s25main/figures/nofHunter.cpp index 80a57c8a79..0d9e096c4b 100644 --- a/libs/s25main/figures/nofHunter.cpp +++ b/libs/s25main/figures/nofHunter.cpp @@ -129,37 +129,55 @@ 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 square around building (actually should be circle, but animals are moving anyway) - const int SQUARE_SIZE = 19; + const auto pointToAnimal = [&world](const MapPoint pt, unsigned) -> noAnimal* { + for(auto& figure : world.GetFigures(pt)) + { + if(figure.GetType() == NodalObjectType::Animal) + return static_cast(&figure); + } + return nullptr; + }; + + const auto canAnimalBeUsed = [pos, maxDistance, &world, isValidAnimal](const noAnimal* const animal) { + return animal && isValidAnimal(animal) + && (pos == animal->GetPos() || world.FindHumanPath(pos, animal->GetPos(), maxDistance)); + }; + + return world.GetPointsInRadius(pos, radius, pointToAnimal, canAnimalBeUsed, true); +} - // Liste mit den gefundenen Tieren +void nofHunter::TryStartHunting() +{ 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) + if(world->GetReplayMinorVersion() >= 4) { - for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x) + 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) { - MapPoint curMapPos = world->MakeMapPoint(curPos); - - // nach Tieren suchen - for(auto& figure : world->GetFigures(curMapPos)) + for(curPos.x = pos.x - SQUARE_SIZE; curPos.x <= pos.x + SQUARE_SIZE; ++curPos.x) { - 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)) + MapPoint curMapPos = world->MakeMapPoint(curPos); + for(auto& figure : world->GetFigures(curMapPos)) { - // Dann nehmen wir es - available_animals.push_back(&animal); + 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); + } } } } 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 dbb02f61ff..13769c3bac 100644 --- a/libs/s25main/figures/nofSkinner.cpp +++ b/libs/s25main/figures/nofSkinner.cpp @@ -10,12 +10,14 @@ #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" #include "random/Random.h" #include "world/GameWorld.h" #include "nodeObjs/noAnimal.h" +#include "gameData/GameConsts.h" #include "gameData/JobConsts.h" using namespace leatheraddon; @@ -187,24 +189,8 @@ 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)) - { - 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 47954cff7f..0bd5cbda3d 100644 --- a/libs/s25main/gameData/GameConsts.h +++ b/libs/s25main/gameData/GameConsts.h @@ -41,6 +41,9 @@ constexpr auto gfs_to_duration(const unsigned gfs) /// Reichweite der Bergarbeiter constexpr unsigned MINER_RADIUS = 2; +/// Search radius for animals (hunter, skinner, AI) +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; diff --git a/libs/s25main/network/GameClient.cpp b/libs/s25main/network/GameClient.cpp index 0060d5964f..4d2a50036b 100644 --- a/libs/s25main/network/GameClient.cpp +++ b/libs/s25main/network/GameClient.cpp @@ -341,6 +341,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 0a9ed60e36..3888f55b87 100644 --- a/libs/s25main/world/GameWorldBase.h +++ b/libs/s25main/world/GameWorldBase.h @@ -67,6 +67,8 @@ class GameWorldBase : public World GameInterface* gi; std::unique_ptr econHandler; std::unique_ptr tradePathCache; + /// 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); @@ -162,6 +164,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 bc83245bd6..e6a98f1e87 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 gameWorld.GetPlayer(i).MakeStartPacts(); } + gameWorld.SetReplayMinorVersion(replay.GetMinorVersion()); gameWorld.InitAfterLoad(); bool endOfReplay = false;