Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions libs/s25main/addons/AddonFreeHarborSpots.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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, _("Build harbors without map markers"),
_("Allows harbors on suitable coastal castle sites even if the map does not define harbor spots."))
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{}
};
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
25 changes: 22 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 allowFreeHarborSpots = false)
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
: world(world), allowFreeHarborSpots(allowFreeHarborSpots)
{}

template<typename T_IsOnRoad>
BuildingQuality operator()(MapPoint pt, T_IsOnRoad isOnRoad, bool flagOnly = false) const;

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

template<typename T_IsOnRoad>
Expand Down Expand Up @@ -219,8 +222,24 @@ 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)
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{
bool isHarborPoint = world.GetNode(pt).harborId.isValid();
if(!isHarborPoint && allowFreeHarborSpots)
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{
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]))
{
isHarborPoint = true;
break;
}
}
}
if(isHarborPoint)
curBQ = BuildingQuality::Harbor;
}

//////////////////////////////////////////////////////////////////////////
// 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()))
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{
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
101 changes: 101 additions & 0 deletions tests/s25Main/integration/testSeaWorldCreation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
// 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/SeaWorldWithGCExecution.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 +66,63 @@ 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);
}
} // namespace

BOOST_AUTO_TEST_CASE(GetShipDir)
Expand Down Expand Up @@ -128,6 +190,45 @@ BOOST_FIXTURE_TEST_CASE(HarborSpotCreation, SeaWorldWithGCExecution<>)
}
}

BOOST_FIXTURE_TEST_CASE(FreeHarborSpotsAddonAddsCoastalHarbors, SeaWorldWithGCExecution<>)
Comment thread
DevOpsOfChaos marked this conversation as resolved.
Outdated
{
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, SeaWorldWithGCExecution<>)
{
createMarkerlessIslandWorld(world);
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