Skip to content

Commit 4149525

Browse files
committed
Fix ship path finding for long distances
Do not try to limit pathfinding for ships. Instead store the most common routes: Those between harbors in a small, fast lookup table. For other routes, e.g. from shipyards or lost ships, use a LRU cache and fall back to unlimited search.
1 parent 46a2045 commit 4149525

6 files changed

Lines changed: 224 additions & 33 deletions

File tree

libs/s25main/Pathfinding.cpp

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
#include "GamePlayer.h"
66
#include "helpers/EnumRange.h"
7+
#include "helpers/IdRange.h"
78
#include "pathfinding/FreePathFinder.h"
89
#include "pathfinding/FreePathFinderImpl.h"
910
#include "pathfinding/PathConditionHuman.h"
1011
#include "pathfinding/PathConditionShip.h"
1112
#include "pathfinding/PathConditionTrade.h"
1213
#include "pathfinding/RoadPathFinder.h"
14+
#include "pathfinding/ShipPathData.h"
1315
#include "world/GameWorld.h"
1416
#include "gameTypes/ShipDirection.h"
1517
#include "gameData/GameConsts.h"
@@ -51,11 +53,10 @@ RoadPathDirection GameWorld::FindPathForWareOnRoads(const noRoadNode& start, con
5153
}
5254

5355
bool GameWorldBase::FindShipPathToHarbor(const MapPoint start, HarborId harborId, SeaId seaId,
54-
std::vector<Direction>* route, unsigned* length)
56+
std::vector<Direction>* route, unsigned* length) const
5557
{
56-
// Find the distance to the furthest harbor from the target harbor and take that as maximum
57-
unsigned maxDistance = 0;
5858
const MapPoint coastalPoint = GetCoastalPoint(harborId, seaId);
59+
RTTR_Assert(coastalPoint.isValid());
5960

6061
// already arrived?
6162
if(start == coastalPoint)
@@ -66,23 +67,52 @@ bool GameWorldBase::FindShipPathToHarbor(const MapPoint start, HarborId harborId
6667
route->clear();
6768
return true;
6869
}
70+
auto& shipPathData = GetShipPathData();
71+
// If we start from a harbor we can get the route directly w/o the costly pathfinding
72+
for(const auto startHbId : helpers::idRange<HarborId>(GetNumHarborPoints()))
73+
{
74+
if(GetCoastalPoint(startHbId, seaId) == start)
75+
{
76+
auto hbRoute = shipPathData.getHarborConnection(startHbId, harborId, seaId);
77+
if(length)
78+
*length = hbRoute.size();
79+
if(route)
80+
*route = std::move(hbRoute);
81+
}
82+
}
6983

70-
for(const auto dir : helpers::EnumRange<ShipDirection>{})
84+
// Try cache first
85+
bool reversed = false;
86+
if(const auto* cachedPath = shipPathData.findCachedPath(start, coastalPoint, reversed))
7187
{
72-
const std::vector<HarborPos::Neighbor>& neighbors = GetHarborNeighbors(harborId, dir);
73-
for(const HarborPos::Neighbor& neighbor : neighbors)
88+
if(route)
7489
{
75-
if(IsHarborAtSea(neighbor.id, seaId) && neighbor.distance > maxDistance)
76-
maxDistance = neighbor.distance;
90+
*route = *cachedPath;
91+
if(reversed)
92+
{
93+
std::reverse(route->begin(), route->end());
94+
for(auto& d : *route)
95+
d = d + 3u; // reverse direction
96+
}
7797
}
98+
if(length)
99+
*length = cachedPath->size();
100+
return true;
78101
}
79-
// Add a few fields reserve
80-
maxDistance += 6;
81-
return FindShipPath(start, coastalPoint, maxDistance, route, length);
102+
103+
// Not in cache -> compute full path and add
104+
std::vector<Direction> newRoute;
105+
if(!FindShipPath(start, coastalPoint, std::numeric_limits<unsigned>::max(), &newRoute, length))
106+
return false;
107+
shipPathData.addToCache(start, coastalPoint, newRoute);
108+
if(route)
109+
*route = std::move(newRoute);
110+
111+
return true;
82112
}
83113

84114
bool GameWorldBase::FindShipPath(const MapPoint start, const MapPoint dest, unsigned maxDistance,
85-
std::vector<Direction>* route, unsigned* length)
115+
std::vector<Direction>* route, unsigned* length) const
86116
{
87117
return GetFreePathFinder().FindPath(start, dest, true, maxDistance, route, length, nullptr,
88118
PathConditionShip(*this));

libs/s25main/nodeObjs/noShip.cpp

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "notifications/ShipNote.h"
2323
#include "ogl/glArchivItem_Bitmap.h"
2424
#include "ogl/glArchivItem_Bitmap_Player.h"
25+
#include "pathfinding/ShipPathData.h"
2526
#include "postSystem/ShipPostMsg.h"
2627
#include "random/Random.h"
2728
#include "world/GameWorld.h"
@@ -952,7 +953,6 @@ void noShip::AbortSeaAttack()
952953
}
953954
}
954955

955-
/// Fängt an zu einem Hafen zu fahren (berechnet Route usw.)
956956
void noShip::StartDrivingToHarborPlace()
957957
{
958958
if(!goalHarbor)
@@ -967,28 +967,28 @@ void noShip::StartDrivingToHarborPlace()
967967
route_.clear();
968968
else
969969
{
970-
bool routeFound;
971-
// Use upper bound to distance by checking the distance between the harbors if we still have and are at the home
972-
// harbor
970+
// if we still have and are at the home harbor get route directly
973971
if(homeHarbor && pos == world->GetCoastalPoint(homeHarbor, seaId_))
974972
{
975-
// Use the maximum distance between the harbors plus 6 fields
976-
unsigned maxDistance = world->GetMinHarborDistance(homeHarbor, goalHarbor) + 6;
977-
routeFound = world->FindShipPath(pos, coastalPos, maxDistance, &route_, nullptr);
973+
route_ = world->GetShipPathData().getHarborConnection(homeHarbor, goalHarbor, seaId_);
974+
RTTR_Assert(!route_.empty());
978975
} else
979-
routeFound = world->FindShipPathToHarbor(pos, goalHarbor, seaId_, &route_, nullptr);
980-
if(!routeFound)
981976
{
982-
// todo
983-
RTTR_Assert(false);
984-
LOG.write("WARNING: Bug detected (GF: %u). Please report this with the savegame and "
985-
"replay.\nnoShip::StartDrivingToHarborPlace: Schiff hat keinen Weg gefunden!\nplayer %i state %i "
986-
"pos %u,%u goal "
987-
"coastal %u,%u goal-id %i goalpos %u,%u \n")
988-
% GetEvMgr().GetCurrentGF() % unsigned(ownerId_) % unsigned(state) % pos.x % pos.y % coastalPos.x
989-
% coastalPos.y % goalHarbor % world->GetHarborPoint(goalHarbor).x % world->GetHarborPoint(goalHarbor).y;
990-
goalHarbor.reset();
991-
return;
977+
if(!world->FindShipPathToHarbor(pos, goalHarbor, seaId_, &route_, nullptr))
978+
{
979+
// todo
980+
RTTR_Assert(false);
981+
LOG.write(
982+
"WARNING: Bug detected (GF: %u). Please report this with the savegame and "
983+
"replay.\nnoShip::StartDrivingToHarborPlace: Schiff hat keinen Weg gefunden!\nplayer %i state %i "
984+
"pos %u,%u goal "
985+
"coastal %u,%u goal-id %i goalpos %u,%u \n")
986+
% GetEvMgr().GetCurrentGF() % unsigned(ownerId_) % unsigned(state) % pos.x % pos.y % coastalPos.x
987+
% coastalPos.y % goalHarbor % world->GetHarborPoint(goalHarbor).x
988+
% world->GetHarborPoint(goalHarbor).y;
989+
goalHarbor.reset();
990+
return;
991+
}
992992
}
993993
}
994994
curRouteIdx = 0;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2+
//
3+
// SPDX-License-Identifier: GPL-2.0-or-later
4+
5+
#include "ShipPathData.h"
6+
#include "EventManager.h"
7+
#include "helpers/IdRange.h"
8+
#include "helpers/containerUtils.h"
9+
#include "pathfinding/FreePathFinder.h"
10+
#include "pathfinding/PathConditionShip.h"
11+
#include "world/GameWorldBase.h"
12+
#include <algorithm>
13+
#include <set>
14+
15+
struct ShipPathData::HarborConnection::Matcher
16+
{
17+
HarborId harbor;
18+
SeaId sea;
19+
bool operator()(const ShipPathData::HarborConnection& con) { return con.destHarbor == harbor && con.sea == sea; }
20+
};
21+
22+
std::vector<Direction> ShipPathData::getHarborConnection(HarborId startHarbor, HarborId destHarbor, SeaId seaId)
23+
{
24+
if(harborConnections_.empty())
25+
initHarborConnections();
26+
const auto it = helpers::find_if(harborConnections_[startHarbor], HarborConnection::Matcher{destHarbor, seaId});
27+
RTTR_Assert(it != harborConnections_[startHarbor].end());
28+
return it->route;
29+
}
30+
31+
const std::vector<Direction>* ShipPathData::findCachedPath(MapPoint start, MapPoint dest, bool& reversed) const
32+
{
33+
for(unsigned i = 0; i < cachedPaths_.size(); ++i)
34+
{
35+
const PathEntry& e = cachedPaths_[i];
36+
const_cast<PathEntry&>(e).lastUse = world_.GetEvMgr().GetCurrentGF();
37+
if(e.start == start && e.dest == dest)
38+
{
39+
reversed = false;
40+
return &e.route;
41+
}
42+
if(e.start == dest && e.dest == start)
43+
{
44+
reversed = true;
45+
return &e.route;
46+
}
47+
}
48+
return nullptr;
49+
}
50+
51+
void ShipPathData::addToCache(MapPoint start, MapPoint dest, std::vector<Direction> route)
52+
{
53+
PathEntry entry{start, dest, world_.GetEvMgr().GetCurrentGF(), std::move(route)};
54+
if(cachedPaths_.size() < cachedPaths_.max_size())
55+
cachedPaths_.emplace_back(std::move(entry));
56+
else
57+
{
58+
const auto itOldest =
59+
std::min_element(cachedPaths_.begin(), cachedPaths_.end(),
60+
[](const PathEntry& a, const PathEntry& b) { return a.lastUse < b.lastUse; });
61+
*itOldest = std::move(entry);
62+
}
63+
}
64+
65+
void ShipPathData::initHarborConnections()
66+
{
67+
harborConnections_.resize(world_.GetNumHarborPoints());
68+
for(const auto startHbId : helpers::idRange<HarborId>(world_.GetNumHarborPoints()))
69+
{
70+
auto& curConnections = harborConnections_[startHbId];
71+
std::vector<HarborPos::Neighbor> neighbors;
72+
for(const auto dir : helpers::EnumRange<ShipDirection>{})
73+
{
74+
const auto& curNbs = world_.GetHarborNeighbors(startHbId, dir);
75+
neighbors.insert(neighbors.end(), curNbs.begin(), curNbs.end());
76+
}
77+
for(const auto& nb : neighbors)
78+
{
79+
RTTR_Assert(startHbId != nb.id);
80+
RTTR_Assert(nb.sea);
81+
// Connections are unique
82+
RTTR_Assert(!helpers::contains_if(curConnections, HarborConnection::Matcher{nb.id, nb.sea}));
83+
curConnections.push_back(HarborConnection{nb.id, nb.sea});
84+
unsigned len;
85+
const bool found =
86+
world_.FindShipPath(world_.GetCoastalPoint(startHbId, nb.sea), world_.GetCoastalPoint(nb.id, nb.sea),
87+
nb.distance, &curConnections.back().route, &len);
88+
RTTR_Assert(found);
89+
RTTR_Assert(len == nb.distance);
90+
}
91+
}
92+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2+
//
3+
// SPDX-License-Identifier: GPL-2.0-or-later
4+
5+
#pragma once
6+
#include <boost/container/small_vector.hpp>
7+
#include <boost/container/static_vector.hpp>
8+
#include <gameTypes/Direction.h>
9+
#include <gameTypes/MapCoordinates.h>
10+
#include <gameTypes/MapTypes.h>
11+
#include <helpers/StrongIdVector.h>
12+
#include <vector>
13+
14+
class GameWorldBase;
15+
16+
/// Implements data for finding ship paths.
17+
/// Assumes seas and land don't change so paths can be heavily cached.
18+
/// Most paths are between harbor points and can be queried directly, for others a LRU cache is provided
19+
class ShipPathData
20+
{
21+
public:
22+
ShipPathData(const GameWorldBase& world) : world_(world) {}
23+
/// Get route from start to destination harbor on the given sea.
24+
/// Assumes it exists, i.e. they both have a coast at that sea.
25+
std::vector<Direction> getHarborConnection(HarborId startHarbor, HarborId destHarbor, SeaId seaId);
26+
/// Find a route in the cache
27+
/// Return reference to it if found, else nullptr.
28+
/// Sets `reversed` to true if start and dest of the route are swapped
29+
const std::vector<Direction>* findCachedPath(MapPoint start, MapPoint dest, bool& reversed) const;
30+
void addToCache(MapPoint start, MapPoint dest, std::vector<Direction> route);
31+
32+
private:
33+
const GameWorldBase& world_;
34+
struct HarborConnection
35+
{
36+
HarborId destHarbor;
37+
SeaId sea;
38+
std::vector<Direction> route;
39+
struct Matcher;
40+
};
41+
42+
/// Connections from a harbor to others.
43+
/// Usually only a small number of harbors are reachable, so inline some.
44+
using HarborConnectionVector = boost::container::small_vector<HarborConnection, 8>;
45+
helpers::StrongIdVector<HarborConnectionVector, HarborId> harborConnections_;
46+
47+
struct PathEntry
48+
{
49+
MapPoint start;
50+
MapPoint dest;
51+
unsigned lastUse;
52+
std::vector<Direction> route;
53+
};
54+
// LRU cache for arbitrary ship paths (start,dest).
55+
boost::container::static_vector<PathEntry, 16> cachedPaths_;
56+
57+
void initHarborConnections();
58+
};

libs/s25main/world/GameWorldBase.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include "notifications/PlayerNodeNote.h"
2323
#include "pathfinding/FreePathFinder.h"
2424
#include "pathfinding/RoadPathFinder.h"
25+
#include "pathfinding/ShipPathData.h"
2526
#include "nodeObjs/noFlag.h"
2627
#include "gameData/BuildingProperties.h"
2728
#include "gameData/GameConsts.h"
@@ -81,6 +82,13 @@ bool GameWorldBase::IsSinglePlayer() const
8182
return true;
8283
}
8384

85+
ShipPathData& GameWorldBase::GetShipPathData() const
86+
{
87+
if(!shipPathData)
88+
shipPathData = std::make_unique<ShipPathData>(*this);
89+
return *shipPathData;
90+
}
91+
8492
bool GameWorldBase::IsRoadAvailable(const bool boat_road, const MapPoint pt) const
8593
{
8694
// Hindernisse

libs/s25main/world/GameWorldBase.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <set>
1717
#include <vector>
1818

19+
class ShipPathData;
1920
class EventManager;
2021
class FreePathFinder;
2122
class GameInterface;
@@ -51,6 +52,7 @@ class GameWorldBase : public World
5152
{
5253
std::unique_ptr<RoadPathFinder> roadPathFinder;
5354
std::unique_ptr<FreePathFinder> freePathFinder;
55+
mutable std::unique_ptr<ShipPathData> shipPathData;
5456
PostManager postManager;
5557
mutable NotificationManager notifications;
5658

@@ -114,12 +116,13 @@ class GameWorldBase : public World
114116
/// Find path for ships to a specific harbor and see. Return true on success
115117
/// If starting point equals the coastal point of target harbor, set length 0, clear route and return true.
116118
bool FindShipPathToHarbor(MapPoint start, HarborId harborId, SeaId seaId, std::vector<Direction>* route,
117-
unsigned* length);
119+
unsigned* length) const;
118120
/// Find path for ships with a limited distance. Return true on success
119121
bool FindShipPath(MapPoint start, MapPoint dest, unsigned maxDistance, std::vector<Direction>* route,
120-
unsigned* length);
122+
unsigned* length) const;
121123
RoadPathFinder& GetRoadPathFinder() const { return *roadPathFinder; }
122124
FreePathFinder& GetFreePathFinder() const { return *freePathFinder; }
125+
ShipPathData& GetShipPathData() const;
123126

124127
/// Return flag that is on road at given point. dir will be set to the direction of the road from the returned flag
125128
/// prevDir (if set) will be skipped when searching for the road points

0 commit comments

Comments
 (0)