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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be in here as it slows down processing during the game. This can be fully done at the place where this information is required and it makes the "keep in sync" part easier as both will be in the same file

{
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()))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be unnecessarily slow: You can simply collect all harbor positions in a vector and use helpers::contains, can't you?

{
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