diff --git a/libs/s25main/GlobalGameSettings.cpp b/libs/s25main/GlobalGameSettings.cpp index 63f240221e..a092dfe9b5 100644 --- a/libs/s25main/GlobalGameSettings.cpp +++ b/libs/s25main/GlobalGameSettings.cpp @@ -75,6 +75,7 @@ void GlobalGameSettings::registerAllAddons() AddonDurableGeologistSigns, AddonEconomyModeGameLength, AddonExhaustibleWater, + AddonFreeHarborSpots, AddonFrontierDistanceReachable, AddonHalfCostMilEquip, AddonInexhaustibleFish, diff --git a/libs/s25main/addons/AddonFreeHarborSpots.h b/libs/s25main/addons/AddonFreeHarborSpots.h new file mode 100644 index 0000000000..86c409dae2 --- /dev/null +++ b/libs/s25main/addons/AddonFreeHarborSpots.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 AddonFreeHarborSpots : public AddonBool +{ +public: + AddonFreeHarborSpots() + : AddonBool(AddonId::FREE_HARBOR_SPOTS, AddonGroup::GamePlay, _("Dangerous: Build harbors without map markers"), + _("Advanced option. Allows harbors on all suitable coastal castle sites even if the map does not " + "define harbor spots. May heavily alter intended map seafaring design.")) + {} +}; diff --git a/libs/s25main/addons/Addons.h b/libs/s25main/addons/Addons.h index 4b773587d0..2d01258e60 100644 --- a/libs/s25main/addons/Addons.h +++ b/libs/s25main/addons/Addons.h @@ -53,6 +53,7 @@ #include "addons/AddonCoinsCapturedBld.h" #include "addons/AddonDemolishBldWORes.h" +#include "addons/AddonFreeHarborSpots.h" #include "addons/AddonFrontierDistanceReachable.h" #include "addons/AddonDurableGeologistSigns.h" diff --git a/libs/s25main/addons/const_addons.h b/libs/s25main/addons/const_addons.h index 4ae5da8c88..f066b0c423 100644 --- a/libs/s25main/addons/const_addons.h +++ b/libs/s25main/addons/const_addons.h @@ -75,7 +75,7 @@ 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, FREE_HARBOR_SPOTS = 0x01000004) //-V:AddonId:801 enum class AddonGroup : unsigned diff --git a/libs/s25main/world/BQCalculator.h b/libs/s25main/world/BQCalculator.h index 88db722d3c..750d77cf81 100644 --- a/libs/s25main/world/BQCalculator.h +++ b/libs/s25main/world/BQCalculator.h @@ -10,13 +10,16 @@ struct BQCalculator { - BQCalculator(const World& world) : world(world) {} + BQCalculator(const World& world, const bool allowHarborsWithoutMapMarkers = false) + : world(world), allowHarborsWithoutMapMarkers(allowHarborsWithoutMapMarkers) + {} template BuildingQuality operator()(MapPoint pt, T_IsOnRoad isOnRoad, bool flagOnly = false) const; private: const World& world; + bool allowHarborsWithoutMapMarkers; }; template @@ -219,8 +222,23 @@ BuildingQuality BQCalculator::operator()(const MapPoint pt, T_IsOnRoad isOnRoad, } // If we can build a castle and this is a harbor point -> Allow harbor - if(curBQ == BuildingQuality::Castle && world.GetNode(pt).harborId) - curBQ = BuildingQuality::Harbor; + if(curBQ == BuildingQuality::Castle) + { + if(world.GetNode(pt).harborId.isValid()) + curBQ = BuildingQuality::Harbor; + else if(allowHarborsWithoutMapMarkers) + { + for(const auto dir : helpers::EnumRange{}) + { + // Keep this in sync with harbor initialization: NW-only coasts are rejected there. + if(dir != Direction::NorthWest && world.GetSeaFromCoastalPoint(neighbours[dir])) + { + curBQ = BuildingQuality::Harbor; + break; + } + } + } + } ////////////////////////////////////////////////////////////////////////// // At this point we can still build a building/mine diff --git a/libs/s25main/world/MapLoader.cpp b/libs/s25main/world/MapLoader.cpp index da4d04a4a3..deaf6809c7 100644 --- a/libs/s25main/world/MapLoader.cpp +++ b/libs/s25main/world/MapLoader.cpp @@ -3,12 +3,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "world/MapLoader.h" +#include "BQCalculator.h" #include "Game.h" #include "GamePlayer.h" #include "GameWorldBase.h" #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" @@ -53,7 +55,7 @@ bool MapLoader::Load(const libsiedler2::ArchivItem_Map& map, Exploration explora return false; PlaceObjects(map); PlaceAnimals(map); - if(!InitSeasAndHarbors(world_)) + if(!InitSeasAndHarbors(world_, std::vector(), world_.GetGGS().isEnabled(AddonId::FREE_HARBOR_SPOTS))) return false; /// Schatten @@ -420,10 +422,39 @@ bool MapLoader::PlaceHQs(GameWorldBase& world, const std::vector& hqPo return true; } -bool MapLoader::InitSeasAndHarbors(World& world, const std::vector& additionalHarbors) +namespace { +bool hasHarborAt(const World& world, const MapPoint pt) +{ + for(const auto harborId : helpers::idRange(world.GetNumHarborPoints())) + { + if(world.GetHarborPoint(harborId) == pt) + return true; + } + return false; +} + +std::vector getGeneratedHarbors(const World& world) +{ + std::vector generatedHarbors; + BQCalculator calcBQ(world, true); + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + if(!hasHarborAt(world, pt) + && calcBQ(pt, [](const MapPoint&) { return false; }) == BuildingQuality::Harbor) + generatedHarbors.push_back(pt); + } + return generatedHarbors; +} +} // namespace + +bool MapLoader::InitSeasAndHarbors(World& world, const std::vector& additionalHarbors, + const bool generateHarborSpots) { for(MapPoint pt : additionalHarbors) - world.harborData.push_back(HarborPos(pt)); + { + if(!hasHarborAt(world, pt)) + world.harborData.push_back(HarborPos(pt)); + } // Clear current harbors and seas RTTR_FOREACH_PT(MapPoint, world.GetSize()) //-V807 { @@ -446,6 +477,12 @@ bool MapLoader::InitSeasAndHarbors(World& world, const std::vector& ad } } + if(generateHarborSpots) + { + for(MapPoint pt : getGeneratedHarbors(world)) + world.harborData.push_back(HarborPos(pt)); + } + /// Determine seas adjacent to the harbor places HarborId curHarborId(1); for(auto it = world.harborData.begin(); it != world.harborData.end();) diff --git a/libs/s25main/world/MapLoader.h b/libs/s25main/world/MapLoader.h index 4dd35e9a36..3415d42682 100644 --- a/libs/s25main/world/MapLoader.h +++ b/libs/s25main/world/MapLoader.h @@ -57,7 +57,8 @@ class MapLoader static void InitShadows(World& world); static void SetMapExplored(World& world); static bool InitSeasAndHarbors(World& world, - const std::vector& additionalHarbors = std::vector()); + const std::vector& additionalHarbors = std::vector(), + bool generateHarborSpots = false); /// 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); diff --git a/tests/s25Main/integration/testSeaWorldCreation.cpp b/tests/s25Main/integration/testSeaWorldCreation.cpp index 499a480b10..58133bcae6 100644 --- a/tests/s25Main/integration/testSeaWorldCreation.cpp +++ b/tests/s25Main/integration/testSeaWorldCreation.cpp @@ -3,10 +3,17 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "RTTR_AssertError.h" +#include "RttrForeachPt.h" #include "helpers/IdRange.h" +#include "addons/const_addons.h" +#include "worldFixtures/CreateSeaWorld.h" #include "worldFixtures/SeaWorldWithGCExecution.h" +#include "worldFixtures/WorldFixture.h" +#include "worldFixtures/terrainHelpers.h" #include "gameTypes/GameTypesOutput.h" #include "gameTypes/ShipDirection.h" +#include "lua/GameDataLoader.h" +#include "world/MapLoader.h" #include #include @@ -61,6 +68,70 @@ void testShipDir(const MapBase& world, const MapPoint fromPt) BOOST_TEST_REQUIRE(getShipDir(world, fromPt, DiffPt(100, -173)) == ShipDirection::NorthEast); BOOST_TEST_REQUIRE(getShipDir(world, fromPt, DiffPt(100, -174)) == ShipDirection::North); } + +void createMarkerlessIslandWorld(GameWorld& world) +{ + world.Unload(); + loadGameData(world.GetDescriptionWriteable()); + world.Init(MapExtent(30, 30)); + + const auto water = GetWaterTerrain(world.GetDescription()); + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + MapNode& node = world.GetNodeWriteable(pt); + node.t1 = node.t2 = water; + } + + const auto land = GetLandTerrain(world.GetDescription(), ETerrain::Buildable); + for(MapPoint pt(8, 8); pt.y < 22; ++pt.y) + { + for(pt.x = 8; pt.x < 22; ++pt.x) + { + MapNode& node = world.GetNodeWriteable(pt); + node.t1 = node.t2 = land; + } + } +} + +unsigned countHarborBQ(const GameWorld& world) +{ + unsigned result = 0; + RTTR_FOREACH_PT(MapPoint, world.GetSize()) + { + if(world.GetNode(pt).bq == BuildingQuality::Harbor) + ++result; + } + return result; +} + +void testHarborPoint(const GameWorld& world, const HarborId harborId) +{ + const MapPoint harborPt = world.GetHarborPoint(harborId); + BOOST_TEST_REQUIRE(harborPt.isValid()); + BOOST_TEST_REQUIRE(world.GetHarborPointID(harborPt) == harborId); + + bool hasSea = false; + for(const auto dir : helpers::EnumRange{}) + { + const SeaId seaId = world.GetSeaId(harborId, dir); + if(!seaId) + continue; + + hasSea = true; + const MapPoint coastalPt = world.GetCoastalPoint(harborId, seaId); + BOOST_TEST_REQUIRE(coastalPt.isValid()); + BOOST_TEST_REQUIRE(world.GetSeaFromCoastalPoint(coastalPt) == seaId); + } + BOOST_TEST_REQUIRE(hasSea); + BOOST_TEST_REQUIRE(world.GetNode(harborPt).bq == BuildingQuality::Harbor); +} + +using SeaWorldFixture = WorldFixture; + +struct MarkerlessIslandFixture : WorldFixtureBase +{ + MarkerlessIslandFixture() : WorldFixtureBase(3) { createMarkerlessIslandWorld(world); } +}; } // namespace BOOST_AUTO_TEST_CASE(GetShipDir) @@ -128,6 +199,44 @@ BOOST_FIXTURE_TEST_CASE(HarborSpotCreation, SeaWorldWithGCExecution<>) } } +BOOST_FIXTURE_TEST_CASE(FreeHarborSpotsAddonAddsCoastalHarbors, SeaWorldFixture) +{ + const unsigned initialHarbors = world.GetNumHarborPoints(); + + ggs.setSelection(AddonId::FREE_HARBOR_SPOTS, 1); + BOOST_TEST_REQUIRE(MapLoader::InitSeasAndHarbors(world, std::vector(), true)); + world.InitAfterLoad(); + + BOOST_TEST_REQUIRE(world.GetNumHarborPoints() > initialHarbors); + for(unsigned harborIdx = initialHarbors + 1; harborIdx <= world.GetNumHarborPoints(); ++harborIdx) + { + testHarborPoint(world, HarborId(harborIdx)); + } +} + +BOOST_FIXTURE_TEST_CASE(FreeHarborSpotsAddonWorksWithoutMapMarkers, MarkerlessIslandFixture) +{ + BOOST_TEST_REQUIRE(MapLoader::InitSeasAndHarbors(world)); + world.InitAfterLoad(); + BOOST_TEST_REQUIRE(world.GetNumHarborPoints() == 0u); + BOOST_TEST_REQUIRE(countHarborBQ(world) == 0u); + + ggs.setSelection(AddonId::FREE_HARBOR_SPOTS, 1); + BOOST_TEST_REQUIRE(MapLoader::InitSeasAndHarbors(world)); + world.InitAfterLoad(); + BOOST_TEST_REQUIRE(world.GetNumHarborPoints() == 0u); + BOOST_TEST_REQUIRE(countHarborBQ(world) == 0u); + + BOOST_TEST_REQUIRE(MapLoader::InitSeasAndHarbors(world, std::vector(), true)); + world.InitAfterLoad(); + + BOOST_TEST_REQUIRE(world.GetNumHarborPoints() > 0u); + for(const auto harborId : helpers::idRange(world.GetNumHarborPoints())) + { + testHarborPoint(world, harborId); + } +} + BOOST_FIXTURE_TEST_CASE(HarborNeighbors, SeaWorldWithGCExecution<>) { // Now just test some assumptions: 2 harbor spots per possible HQ.