diff --git a/external/turtle b/external/turtle index 2a9438ea7e..b53fc3c824 160000 --- a/external/turtle +++ b/external/turtle @@ -1 +1 @@ -Subproject commit 2a9438ea7ed573068fccbfa72176e24239a026d4 +Subproject commit b53fc3c8240111647b8c989d0d4dca45a325bd96 diff --git a/libs/s25main/CheatCommandTracker.cpp b/libs/s25main/CheatCommandTracker.cpp index 045a2c59f3..a4f59dd90f 100644 --- a/libs/s25main/CheatCommandTracker.cpp +++ b/libs/s25main/CheatCommandTracker.cpp @@ -57,6 +57,7 @@ void CheatCommandTracker::onSpecialKeyEvent(const KeyEvent& ke) switch(ke.kt) { + case KeyType::F6: cheats_.toggleHumanAIPlayer(); break; case KeyType::F7: { if(ke.alt) @@ -65,7 +66,6 @@ void CheatCommandTracker::onSpecialKeyEvent(const KeyEvent& ke) cheats_.toggleAllVisible(); } break; - case KeyType::F10: cheats_.toggleHumanAIPlayer(); break; default: break; } } diff --git a/libs/s25main/Replay.cpp b/libs/s25main/Replay.cpp index 9171a814f4..d37c9d5cfe 100644 --- a/libs/s25main/Replay.cpp +++ b/libs/s25main/Replay.cpp @@ -40,7 +40,8 @@ uint8_t Replay::GetLatestMinorVersion() const // 8.1: Portraits support // 8.2: Set correct initial distributions if replay starts without savegame for leather addon (see GameClient.cpp // StartReplay function for detailed description) - return 2; + // 8.3 Remove invalid fish for replays started from start (i.e. map instead of savegame) + return 3; } uint8_t Replay::GetLatestMajorVersion() const diff --git a/libs/s25main/figures/nofFisher.h b/libs/s25main/figures/nofFisher.h index ec85bba314..8b9712710a 100644 --- a/libs/s25main/figures/nofFisher.h +++ b/libs/s25main/figures/nofFisher.h @@ -26,9 +26,6 @@ class nofFisher : public nofFarmhand /// Abgeleitete Klasse informieren, wenn fertig ist mit Arbeiten void WorkFinished() override; - /// Returns the quality of this working point or determines if the worker can work here at all - PointQuality GetPointQuality(MapPoint pt, bool isBeforeWork) const override; - public: nofFisher(MapPoint pos, unsigned char player, nobUsual* workplace); nofFisher(SerializedGameData& sgd, unsigned obj_id); @@ -38,4 +35,7 @@ class nofFisher : public nofFarmhand void Serialize(SerializedGameData& sgd) const override; GO_Type GetGOT() const final { return GO_Type::NofFisher; } + + /// Returns the quality of this working point or determines if the worker can work here at all + PointQuality GetPointQuality(MapPoint pt, bool isBeforeWork) const override; }; diff --git a/libs/s25main/network/GameClient.cpp b/libs/s25main/network/GameClient.cpp index 18c451ca74..0060d5964f 100644 --- a/libs/s25main/network/GameClient.cpp +++ b/libs/s25main/network/GameClient.cpp @@ -337,7 +337,9 @@ void GameClient::StartGame(const unsigned random_init) OnError(ClientError::InvalidMap); return; } - gameWorld.SetupResources(); + // TODO (Replay): Always use true + const bool fixFish = !GetReplay() || GetReplay()->GetMinorVersion() >= 3; + MapLoader::SetupResources(gameWorld, fixFish); } gameWorld.InitAfterLoad(); @@ -1872,9 +1874,14 @@ void GameClient::ToggleHumanAIPlayer(const AI::Info& aiInfo) auto it = helpers::find_if(game->aiPlayers_, [id = this->GetPlayerId()](const auto& player) { return player.GetPlayerId() == id; }); if(it != game->aiPlayers_.end()) + { game->aiPlayers_.erase(it); - else + SystemChat(_("Disabled AI for current player")); + } else + { game->AddAIPlayer(CreateAIPlayer(GetPlayerId(), aiInfo)); + SystemChat(_("Enabled AI for current player")); + } } void GameClient::RequestSwapToPlayer(const unsigned char newId) diff --git a/libs/s25main/world/GameWorld.cpp b/libs/s25main/world/GameWorld.cpp index c3f99881ac..1922cfb443 100644 --- a/libs/s25main/world/GameWorld.cpp +++ b/libs/s25main/world/GameWorld.cpp @@ -1294,87 +1294,6 @@ bool GameWorld::IsBorderNode(const MapPoint pt, const unsigned char owner) const return (GetNode(pt).owner == owner && !IsPlayerTerritory(pt, owner)); } -/** - * Konvertiert Ressourcen zwischen Typen hin und her oder löscht sie. - * Für Spiele ohne Gold. - */ -void GameWorld::ConvertMineResourceTypes(ResourceType from, ResourceType to) -{ - // LOG.write(("Convert map resources from %i to %i\n", from, to); - if(from == to) - return; - - RTTR_FOREACH_PT(MapPoint, GetSize()) - { - Resource resources = GetNode(pt).resources; - // Gibt es Ressourcen dieses Typs? - // Wenn ja, dann umwandeln bzw löschen - if(resources.getType() == from) - { - resources.setType(to); - SetResource(pt, resources); - } - } -} - -void GameWorld::SetupResources() -{ - ResourceType target; - switch(GetGGS().getSelection(AddonId::CHANGE_GOLD_DEPOSITS)) - { - case 0: - default: target = ResourceType::Gold; break; - case 1: target = ResourceType::Nothing; break; - case 2: target = ResourceType::Iron; break; - case 3: target = ResourceType::Coal; break; - case 4: target = ResourceType::Granite; break; - } - ConvertMineResourceTypes(ResourceType::Gold, target); - PlaceAndFixWater(); -} - -/** - * Fills water depending on terrain and Addon setting - */ -void GameWorld::PlaceAndFixWater() -{ - bool waterEverywhere = GetGGS().getSelection(AddonId::EXHAUSTIBLE_WATER) == 1; - - RTTR_FOREACH_PT(MapPoint, GetSize()) - { - Resource curNodeResource = GetNode(pt).resources; - - if(curNodeResource.getType() == ResourceType::Nothing) - { - if(!waterEverywhere) - continue; - } else if(curNodeResource.getType() != ResourceType::Water) - { - // do not override maps resource. - continue; - } - - uint8_t minHumidity = 100; - for(const DescIdx tIdx : GetTerrainsAround(pt)) - { - const uint8_t curHumidity = GetDescription().get(tIdx).humidity; - if(curHumidity < minHumidity) - { - minHumidity = curHumidity; - if(minHumidity == 0) - break; - } - } - if(minHumidity) - curNodeResource = - Resource(ResourceType::Water, waterEverywhere ? 7 : helpers::iround(minHumidity * 7. / 100.)); - else - curNodeResource = Resource(ResourceType::Nothing, 0); - - SetResource(pt, curNodeResource); - } -} - /// Gründet vom Schiff aus eine neue Kolonie bool GameWorld::FoundColony(const HarborId harbor, const unsigned char player, const SeaId seaId) { diff --git a/libs/s25main/world/GameWorld.h b/libs/s25main/world/GameWorld.h index 6c811a0505..0686a2c525 100644 --- a/libs/s25main/world/GameWorld.h +++ b/libs/s25main/world/GameWorld.h @@ -145,15 +145,6 @@ class GameWorld : public GameWorldBase /// Return whether this is a border node (node belongs to player, but not all others around) bool IsBorderNode(MapPoint pt, unsigned char owner) const; - // Konvertiert Ressourcen zwischen Typen hin und her oder löscht sie. - // Für Spiele ohne Gold. - void ConvertMineResourceTypes(ResourceType from, ResourceType to); - // Setup resources like gold and water after loading a new map - void SetupResources(); - - // Fills water depending on terrain and Addon setting - void PlaceAndFixWater(); - /// Gründet vom Schiff aus eine neue Kolonie, gibt true zurück bei Erfolg bool FoundColony(HarborId harbor, unsigned char player, SeaId seaId); /// Registriert eine Baustelle eines Hafens, die vom Schiff aus gesetzt worden ist diff --git a/libs/s25main/world/MapLoader.cpp b/libs/s25main/world/MapLoader.cpp index da4d04a4a3..5fd781ce56 100644 --- a/libs/s25main/world/MapLoader.cpp +++ b/libs/s25main/world/MapLoader.cpp @@ -9,9 +9,13 @@ #include "GlobalGameSettings.h" #include "PointOutput.h" #include "RttrForeachPt.h" +#include "addons/const_addons.h" #include "buildings/nobHQ.h" #include "factories/BuildingFactory.h" #include "helpers/IdRange.h" +#include "helpers/Range.h" +#include "helpers/containerUtils.h" +#include "helpers/mathFuncs.h" #include "lua/GameDataLoader.h" #include "pathfinding/PathConditionShip.h" #include "random/Random.h" @@ -106,6 +110,105 @@ bool MapLoader::PlaceHQs(bool addStartWares) return PlaceHQs(world_, hqPositions, addStartWares); } +void MapLoader::SetupResources(GameWorldBase& world, const bool fixFish) +{ + ResourceType target; + switch(world.GetGGS().getSelection(AddonId::CHANGE_GOLD_DEPOSITS)) + { + case 0: + default: target = ResourceType::Gold; break; + case 1: target = ResourceType::Nothing; break; + case 2: target = ResourceType::Iron; break; + case 3: target = ResourceType::Coal; break; + case 4: target = ResourceType::Granite; break; + } + ConvertMineResourceTypes(world, ResourceType::Gold, target); + PlaceAndFixWater(world); + if(fixFish) + RemoveUnusableFishResources(world); +} + +void MapLoader::ConvertMineResourceTypes(GameWorldBase& world, ResourceType from, ResourceType to) +{ + // LOG.write(("Convert map resources from %i to %i\n", from, to); + if(from == to) + return; + + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + Resource resources = world.GetNode(pt).resources; + // Gibt es Ressourcen dieses Typs? + // Wenn ja, dann umwandeln bzw löschen + if(resources.getType() == from) + { + resources.setType(to); + world.SetResource(pt, resources); + } + } +} + +void MapLoader::PlaceAndFixWater(GameWorldBase& world) +{ + const bool waterEverywhere = world.GetGGS().getSelection(AddonId::EXHAUSTIBLE_WATER) == 1; + + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + Resource curNodeResource = world.GetNode(pt).resources; + + if(curNodeResource.getType() == ResourceType::Nothing) + { + if(!waterEverywhere) + continue; + } else if(curNodeResource.getType() != ResourceType::Water) + continue; // do not override maps resource. + + uint8_t minHumidity = 100; + for(const DescIdx tIdx : world.GetTerrainsAround(pt)) + { + const uint8_t curHumidity = world.GetDescription().get(tIdx).humidity; + if(curHumidity < minHumidity) + { + minHumidity = curHumidity; + if(minHumidity == 0) + break; + } + } + if(minHumidity) + { + curNodeResource = + Resource(ResourceType::Water, waterEverywhere ? 7 : helpers::iround(minHumidity * 7. / 100.)); + } else + curNodeResource = Resource(ResourceType::Nothing, 0); + + world.SetResource(pt, curNodeResource); + } +} + +void MapLoader::RemoveUnusableFishResources(GameWorldBase& world) +{ + const auto isWaterPoint = [&world](const MapPoint nb) { return world.IsWaterPoint(nb); }; + for(const MapCoord y : helpers::range(world.GetHeight())) + { + // Optimization: When there was fish on the previous node (in the same row) + // we do not need to check for isolated water points, as there is at least that water point + bool previousHasFish = false; + for(const MapCoord x : helpers::range(world.GetWidth())) + { + const MapPoint pt(x, y); + bool hasFish = false; + + if(world.GetNode(pt).resources.has(ResourceType::Fish)) + { + if(isWaterPoint(pt) && (previousHasFish || helpers::contains_if(world.GetNeighbours(pt), isWaterPoint))) + hasFish = true; + else + world.SetResource(pt, Resource(ResourceType::Nothing, 0)); + } + previousHasFish = hasFish; + } + } +} + void MapLoader::InitShadows(World& world) { RTTR_FOREACH_PT(MapPoint, world.GetSize()) @@ -117,7 +220,7 @@ void MapLoader::SetMapExplored(World& world) RTTR_FOREACH_PT(MapPoint, world.GetSize()) { // For every player - for(unsigned i = 0; i < MAX_PLAYERS; ++i) + for(const auto i : helpers::range(MAX_PLAYERS)) { // If we have FoW here, save it if(world.GetNode(pt).fow[i].visibility == Visibility::FogOfWar) diff --git a/libs/s25main/world/MapLoader.h b/libs/s25main/world/MapLoader.h index 4dd35e9a36..44d52d2a77 100644 --- a/libs/s25main/world/MapLoader.h +++ b/libs/s25main/world/MapLoader.h @@ -7,6 +7,7 @@ #include "gameTypes/GameSettingTypes.h" #include "gameTypes/MapCoordinates.h" #include "gameTypes/MapTypes.h" +#include "gameTypes/Resource.h" #include "gameData/DescIdx.h" #include #include @@ -50,6 +51,9 @@ class MapLoader /// Optionally add the starting wares to the HQs. /// Return false if there was an error (e.g. invalid start position) bool PlaceHQs(bool addStartWares = true); + /// Setup resources like gold and water after loading a new map. + /// TODO(Replay): Remove fixFish (always set to true) + static void SetupResources(GameWorldBase& world, bool fixFish = true); /// Return the (original/unshuffled) position of the players HQ (only valid after successful load) MapPoint GetOriginalHQPos(unsigned player) const { return hqPositions_[player]; } @@ -61,4 +65,12 @@ class MapLoader /// Place the HQs on a loaded map and add starting wares if desired. /// Return false if there was an error. static bool PlaceHQs(GameWorldBase& world, const std::vector& hqPositions, bool addStartWares = true); + +private: + /// Converts map resources between types or deletes them. Used for games without gold. + static void ConvertMineResourceTypes(GameWorldBase& world, ResourceType from, ResourceType to); + /// Fills water depending on terrain and Addon setting. + static void PlaceAndFixWater(GameWorldBase& world); + /// Removes fish resources that cannot be reached by fisheries. + static void RemoveUnusableFishResources(GameWorldBase& world); }; diff --git a/tests/s25Main/autoplay/main.cpp b/tests/s25Main/autoplay/main.cpp index 8454eb2226..bc83245bd6 100644 --- a/tests/s25Main/autoplay/main.cpp +++ b/tests/s25Main/autoplay/main.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2005 - 2024 Settlers Freaks (sf-team at siedler25.org) +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) // // SPDX-License-Identifier: GPL-2.0-or-later @@ -7,6 +7,7 @@ #include "Game.h" #include "GamePlayer.h" #include "Replay.h" +#include "Savegame.h" #include "Timer.h" #include "helpers/chronoIO.h" #include "network/PlayerGameCommands.h" @@ -21,6 +22,7 @@ #include "libsiedler2/libsiedler2.h" #include "s25util/tmpFile.h" #include +#include #include #if RTTR_HAS_VLD @@ -31,6 +33,14 @@ struct Fixture : rttr::test::Fixture { Fixture() { libsiedler2::setAllocator(new GlAllocator); } }; +struct MockGameState : ILocalGameState +{ +public: + unsigned GetPlayerId() const override { return 0; } + bool IsHost() const override { return true; } + std::string FormatGFTime(unsigned) const override { return ""; } + void SystemChat(const std::string&) override {} +}; BOOST_GLOBAL_FIXTURE(Fixture); static boost::test_tools::predicate_result verifyChecksum(const AsyncChecksum& actual, const AsyncChecksum& expected, @@ -47,17 +57,12 @@ static boost::test_tools::predicate_result verifyChecksum(const AsyncChecksum& a // LCOV_EXCL_STOP } -static void playReplay(const boost::filesystem::path& replayPath) +static void playReplay(const boost::filesystem::path& replayPath, const bool isSavegame) { Replay replay; BOOST_TEST_REQUIRE(replay.LoadHeader(replayPath)); MapInfo mapInfo; BOOST_TEST_REQUIRE(replay.LoadGameData(mapInfo)); - BOOST_TEST_REQUIRE(!mapInfo.savegame); // Must be from start - TmpFile mapfile; - mapfile.close(); - BOOST_TEST_REQUIRE(mapInfo.mapData.DecompressToFile(mapfile.filePath)); - std::vector players; for(unsigned i = 0; i < replay.GetNumPlayers(); i++) players.emplace_back(replay.GetPlayer(i)); @@ -65,12 +70,28 @@ static void playReplay(const boost::filesystem::path& replayPath) RANDOM.Init(replay.getSeed()); GameWorld& gameWorld = game.world_; - for(unsigned i = 0; i < gameWorld.GetNumPlayers(); ++i) - gameWorld.GetPlayer(i).MakeStartPacts(); + if(isSavegame) + { + BOOST_TEST_REQUIRE(mapInfo.savegame); + MockGameState gs; + mapInfo.savegame->sgd.ReadSnapshot(game, gs); + } else + { + TmpFile mapfile; + mapfile.close(); + BOOST_TEST_REQUIRE(!mapInfo.savegame); + BOOST_TEST_REQUIRE(mapInfo.mapData.DecompressToFile(mapfile.filePath)); + MapLoader loader(gameWorld); + BOOST_TEST_REQUIRE(loader.Load(mapfile.filePath)); + // TODO(replay): Since 8.3 invalid fish is removed when starting from map + BOOST_TEST_REQUIRE(replay.GetMajorVersion() == 8u); + BOOST_TEST_REQUIRE(replay.GetMinorVersion() < 3u); + MapLoader::SetupResources(gameWorld, false); + + for(unsigned i = 0; i < gameWorld.GetNumPlayers(); ++i) + gameWorld.GetPlayer(i).MakeStartPacts(); + } - MapLoader loader(gameWorld); - BOOST_TEST_REQUIRE(loader.Load(mapfile.filePath)); - gameWorld.SetupResources(); gameWorld.InitAfterLoad(); bool endOfReplay = false; @@ -111,14 +132,15 @@ static void playReplay(const boost::filesystem::path& replayPath) BOOST_AUTO_TEST_CASE(Play200kReplay) { - // Map: Big Slaughter v2 + // Map: Others/Big Slaughter v2 // 7 x Hard KI // 2 KIs each in Teams 1-3, 1 in Team 4 // Player KI without team ("WINTER" + F10) // Default addon settings + // Save immediately, then load (so savegame is embedded instead of map) // 200k GFs run (+ a bit) const boost::filesystem::path replayPath = rttr::test::rttrBaseDir / "tests" / "testData" / "200kGFs.rpl"; - playReplay(replayPath); + playReplay(replayPath, true); } BOOST_AUTO_TEST_CASE(PlaySeaReplay) @@ -128,5 +150,5 @@ BOOST_AUTO_TEST_CASE(PlaySeaReplay) // No teams, Sea attacks enabled (harbors block), ships fast // 300k GFs run (+ a bit) const boost::filesystem::path replayPath = rttr::test::rttrBaseDir / "tests" / "testData" / "SeaMap300kGfs.rpl"; - playReplay(replayPath); + playReplay(replayPath, false); } diff --git a/tests/s25Main/integration/testBuilding.cpp b/tests/s25Main/integration/testBuilding.cpp index a5603c6b9e..2b8baa78a1 100644 --- a/tests/s25Main/integration/testBuilding.cpp +++ b/tests/s25Main/integration/testBuilding.cpp @@ -6,15 +6,22 @@ #include "PointOutput.h" #include "RttrForeachPt.h" #include "buildings/nobBaseMilitary.h" +#include "buildings/nobUsual.h" #include "desktops/dskGameInterface.h" +#include "factories/BuildingFactory.h" +#include "figures/nofFarmhand.h" +#include "figures/nofFisher.h" #include "helpers/containerUtils.h" #include "uiHelper/uiHelpers.hpp" #include "worldFixtures/CreateEmptyWorld.h" #include "worldFixtures/WorldFixture.h" +#include "worldFixtures/terrainHelpers.h" #include "world/GameWorldViewer.h" +#include "world/MapLoader.h" #include "nodeObjs/noEnvObject.h" #include "nodeObjs/noStaticObject.h" #include "gameTypes/GameTypesOutput.h" +#include "gameTypes/Resource.h" #include // LCOV_EXCL_START @@ -476,4 +483,63 @@ BOOST_FIXTURE_TEST_CASE(RoadRemovesObjs, EmptyWorldFixture1P) } } +BOOST_FIXTURE_TEST_CASE(FisherIgnoresIsolatedFishWater, EmptyWorldFixture1PBiggest) +{ + const DescIdx tWater = GetWaterTerrain(world.GetDescription()); + + const MapPoint fisheryPos = world.MakeMapPoint(world.GetPlayer(0).GetHQPos() - Position(6, 6)); + auto* fishery = dynamic_cast( + BuildingFactory::CreateBuilding(world, BuildingType::Fishery, fisheryPos, 0, Nation::Romans)); + BOOST_TEST_REQUIRE(fishery); + + nofFisher fisher(fisheryPos, 0, fishery); + + const MapPoint workPt = world.MakeMapPoint(fisheryPos + Position(4, 0)); + const MapPoint fishPt = world.GetNeighbour(workPt, Direction::East); + + auto makeWaterPoint = [&](const MapPoint pt) { + MapNode& nwNode = world.GetNodeWriteable(world.GetNeighbour(pt, Direction::NorthWest)); + MapNode& neNode = world.GetNodeWriteable(world.GetNeighbour(pt, Direction::NorthEast)); + MapNode& curNode = world.GetNodeWriteable(pt); + MapNode& wNode = world.GetNodeWriteable(world.GetNeighbour(pt, Direction::West)); + + nwNode.t1 = tWater; + nwNode.t2 = tWater; + neNode.t1 = tWater; + curNode.t1 = tWater; + curNode.t2 = tWater; + wNode.t2 = tWater; + }; + + makeWaterPoint(fishPt); + world.SetResource(fishPt, Resource(ResourceType::Fish, 4)); + MapLoader::SetupResources(world); + + BOOST_TEST_REQUIRE(!world.GetNode(fishPt).resources.has(ResourceType::Fish)); + BOOST_TEST_REQUIRE((fisher.GetPointQuality(workPt, false) == nofFarmhand::PointQuality::NotPossible)); + + const MapPoint connectedFishPt = world.GetNeighbour(fishPt, Direction::East); + makeWaterPoint(connectedFishPt); + world.SetResource(fishPt, Resource(ResourceType::Fish, 4)); + world.SetResource(connectedFishPt, Resource(ResourceType::Fish, 4)); + MapLoader::SetupResources(world); + + BOOST_TEST_REQUIRE(world.GetNode(fishPt).resources.has(ResourceType::Fish)); + BOOST_TEST_REQUIRE(world.GetNode(connectedFishPt).resources.has(ResourceType::Fish)); + BOOST_TEST_REQUIRE((fisher.GetPointQuality(workPt, false) == nofFarmhand::PointQuality::Class1)); + + const MapPoint rowEndFishPt(world.GetWidth() - 1, 10); + const MapPoint rowEndConnectedWaterPt = world.GetNeighbour(rowEndFishPt, Direction::West); + const MapPoint nextRowFishPt(0, rowEndFishPt.y + 1); + + makeWaterPoint(rowEndFishPt); + makeWaterPoint(rowEndConnectedWaterPt); + makeWaterPoint(nextRowFishPt); + world.SetResource(rowEndFishPt, Resource(ResourceType::Fish, 4)); + world.SetResource(nextRowFishPt, Resource(ResourceType::Fish, 4)); + MapLoader::SetupResources(world); + + BOOST_TEST_REQUIRE(world.GetNode(rowEndFishPt).resources.has(ResourceType::Fish)); + BOOST_TEST_REQUIRE(!world.GetNode(nextRowFishPt).resources.has(ResourceType::Fish)); +} BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/testData/200kGFs.rpl b/tests/testData/200kGFs.rpl index 8ff510f769..ec166734d0 100644 Binary files a/tests/testData/200kGFs.rpl and b/tests/testData/200kGFs.rpl differ