Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions libs/s25main/GlobalGameSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ void GlobalGameSettings::registerAllAddons()
AddonDurableGeologistSigns,
AddonEconomyModeGameLength,
AddonExhaustibleWater,
AddonFreeHarborSpots,
AddonFrontierDistanceReachable,
AddonHalfCostMilEquip,
AddonInexhaustibleFish,
Expand Down
18 changes: 18 additions & 0 deletions libs/s25main/addons/AddonFreeHarborSpots.h
Original file line number Diff line number Diff line change
@@ -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."))
{}
};
1 change: 1 addition & 0 deletions libs/s25main/addons/Addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

#include "addons/AddonCoinsCapturedBld.h"
#include "addons/AddonDemolishBldWORes.h"
#include "addons/AddonFreeHarborSpots.h"
#include "addons/AddonFrontierDistanceReachable.h"

#include "addons/AddonDurableGeologistSigns.h"
Expand Down
2 changes: 1 addition & 1 deletion libs/s25main/addons/const_addons.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 21 additions & 3 deletions libs/s25main/world/BQCalculator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename T_IsOnRoad>
BuildingQuality operator()(MapPoint pt, T_IsOnRoad isOnRoad, bool flagOnly = false) const;

private:
const World& world;
bool allowHarborsWithoutMapMarkers;
};

template<typename T_IsOnRoad>
Expand Down Expand Up @@ -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<Direction>{})
{
// 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
Expand Down
43 changes: 40 additions & 3 deletions libs/s25main/world/MapLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<MapPoint>(), world_.GetGGS().isEnabled(AddonId::FREE_HARBOR_SPOTS)))
return false;

/// Schatten
Expand Down Expand Up @@ -420,10 +422,39 @@ bool MapLoader::PlaceHQs(GameWorldBase& world, const std::vector<MapPoint>& hqPo
return true;
}

bool MapLoader::InitSeasAndHarbors(World& world, const std::vector<MapPoint>& additionalHarbors)
namespace {
bool hasHarborAt(const World& world, const MapPoint pt)
{
for(const auto harborId : helpers::idRange<HarborId>(world.GetNumHarborPoints()))
{
if(world.GetHarborPoint(harborId) == pt)
return true;
}
return false;
}

std::vector<MapPoint> getGeneratedHarbors(const World& world)
{
std::vector<MapPoint> 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<MapPoint>& 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
{
Expand All @@ -446,6 +477,12 @@ bool MapLoader::InitSeasAndHarbors(World& world, const std::vector<MapPoint>& 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();)
Expand Down
3 changes: 2 additions & 1 deletion libs/s25main/world/MapLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class MapLoader
static void InitShadows(World& world);
static void SetMapExplored(World& world);
static bool InitSeasAndHarbors(World& world,
const std::vector<MapPoint>& additionalHarbors = std::vector<MapPoint>());
const std::vector<MapPoint>& additionalHarbors = std::vector<MapPoint>(),
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<MapPoint>& hqPositions, bool addStartWares = true);
Expand Down
109 changes: 109 additions & 0 deletions tests/s25Main/integration/testSeaWorldCreation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <rttr/test/LogAccessor.hpp>
#include <boost/test/unit_test.hpp>

Expand Down Expand Up @@ -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<Direction>{})
{
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<CreateSeaWorld, 3, SeaWorldDefault::width, SeaWorldDefault::height>;

struct MarkerlessIslandFixture : WorldFixtureBase
{
MarkerlessIslandFixture() : WorldFixtureBase(3) { createMarkerlessIslandWorld(world); }
};
} // namespace

BOOST_AUTO_TEST_CASE(GetShipDir)
Expand Down Expand Up @@ -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<MapPoint>(), 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<MapPoint>(), true));
world.InitAfterLoad();

BOOST_TEST_REQUIRE(world.GetNumHarborPoints() > 0u);
for(const auto harborId : helpers::idRange<HarborId>(world.GetNumHarborPoints()))
{
testHarborPoint(world, harborId);
}
}

BOOST_FIXTURE_TEST_CASE(HarborNeighbors, SeaWorldWithGCExecution<>)
{
// Now just test some assumptions: 2 harbor spots per possible HQ.
Expand Down