Skip to content
Merged
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
100 changes: 69 additions & 31 deletions src/runtime/components/navigation/Pathfinder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "game/GameLock.h"
#include "game/Level.h"
#include "game/Room.h"
#include "game/Unit.h"
#include "utils/VirtualArray.h"

namespace d2bs::runtime::navigation {
Expand Down Expand Up @@ -161,39 +162,45 @@ std::vector<Point> CollisionLookup::FindPortals(Point start, Point end) const {

// --- Build collision grid from game level ---

LevelGrid BuildLevelGrid(game::Level level) {
LevelGrid grid(level.Bounds(), CollisionFlag::All);
grid.levelId = level.Id();
grid.mapSeed = game::GetMapSeed();
void LevelGrid::CopyRoomCollision(const game::Room& room) {
auto rb = room.Bounds();
uint32_t roomW = rb.size.width;
uint32_t roomH = rb.size.height;
auto collData = room.GetCollisionFlat();
if (roomW == 0 || roomH == 0 || collData.empty())
return;

for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) {
auto rb = room.Bounds();
uint32_t roomW = rb.size.width;
uint32_t roomH = rb.size.height;
auto collData = room.GetCollisionFlat();
if (roomW == 0 || roomH == 0 || collData.empty())
continue;
const auto rows = static_cast<uint32_t>(std::min<size_t>(roomH, collData.size() / roomW));

const auto rows = static_cast<uint32_t>(std::min<size_t>(roomH, collData.size() / roomW));
// Rooms are always fully within their level grid (D2 level layout invariant:
// every room's origin/size is contained by pLevel->dwPosX/Y/SizeX/Y), so the
// Position subtraction inside CellIndex cannot underflow here.
for (uint32_t ry = 0; ry < rows; ry++) {
Position dst{.x = rb.origin.x, .y = rb.origin.y + ry};
std::memcpy(&data[CellIndex(rect, dst)], &collData[ry * roomW], roomW * sizeof(CollisionFlag));
}
MarkBarricades(room);
}

// Rooms are always fully within their level grid (D2 level layout invariant:
// every room's origin/size is contained by pLevel->dwPosX/Y/SizeX/Y), so the
// Position subtraction inside CellIndex cannot underflow here.
for (uint32_t ry = 0; ry < rows; ry++) {
Position dst{.x = rb.origin.x, .y = rb.origin.y + ry};
std::memcpy(&grid.data[CellIndex(grid.rect, dst)], &collData[ry * roomW], roomW * sizeof(CollisionFlag));
void LevelGrid::MarkBarricades(const game::Room& room) {
if (!HasBarricadeOverlay(levelId)) {
return;
}
auto roomPos = room.Bounds().origin;
for (const auto& preset : room.GetPresetUnits()) {
if (preset.id == BARRICADE_TOWER_PRESET_ID) {
Set(roomPos + preset.posInRoom, CollisionFlag::All);
}
}
}

if (HasBarricadeOverlay(level.Id())) {
for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) {
auto roomPos = room.Bounds().origin;
for (const auto& preset : room.GetPresetUnits()) {
if (preset.id == BARRICADE_TOWER_PRESET_ID) {
grid.Set(roomPos + preset.posInRoom, CollisionFlag::All);
}
}
}
LevelGrid BuildLevelGrid(game::Level level) {
LevelGrid grid(level.Bounds(), CollisionFlag::All);
grid.levelId = level.Id();
grid.mapSeed = game::GetMapSeed();

for (auto room = level.GetFirstRoom(); room; room = room.GetNext()) {
grid.CopyRoomCollision(room);
}

return grid;
Expand Down Expand Up @@ -978,12 +985,41 @@ std::vector<Position> FindPathOnGrid(CollisionLookup& collision, Position start,
// game's collision masks, it's safe to keep without holding a GameReadLock
// outside the build path.
//
// Staleness window: doors opening / collision changes inside the same
// level aren't reflected - accepted, getWalkDistance is a heuristic
// check, and A* on actual traversal goes through the cache eviction or
// re-resolve path on each fresh call sequence.
// Reference d2bs reads collision live on every call, so a path asked for
// after a door opens goes through it. Collision only changes where the
// game has units, which on the client is the rooms around the player, so
// a cache hit re-copies the player's room and its neighbours into the
// cached grids. Those rooms are loaded, so this adds no room data.
namespace {
thread_local std::optional<CollisionLookup> cachedLookup;

void RefreshRoomsNearPlayer(CollisionLookup& lookup) {
game::GameReadLock guard;
const auto player = game::Unit::Player();
if (!player) {
return;
}
const auto playerRoom = player.GetRoom();
if (!playerRoom) {
return;
}
auto rooms = playerRoom.GetNearby();
rooms.push_back(playerRoom);
for (const auto& room : rooms) {
const auto levelId = room.GetLevel().Id();
LevelGrid* grid = nullptr;
if (levelId == lookup.primary.levelId) {
grid = &lookup.primary;
} else if (auto it = lookup.secondary.find(levelId); it != lookup.secondary.end()) {
grid = &it->second;
}
if (grid == nullptr) {
continue;
}
grid->CopyRoomCollision(room);
}
}

} // namespace

std::vector<Position> FindPath(const PathRequest& request) {
Expand All @@ -996,6 +1032,8 @@ std::vector<Position> FindPath(const PathRequest& request) {
return {};
cachedLookup.emplace();
cachedLookup->primary = BuildLevelGrid(*levelOpt);
} else {
RefreshRoomsNearPlayer(*cachedLookup);
}

return FindPathOnGrid(*cachedLookup, request.start, request.end, request.reduction, request.radius,
Expand Down
8 changes: 8 additions & 0 deletions src/runtime/components/navigation/Pathfinder.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

namespace d2bs::game {
class Level;
class Room;
} // namespace d2bs::game

namespace d2bs::runtime::navigation {
Expand Down Expand Up @@ -65,6 +66,13 @@ struct LevelGrid {
return Contains(p) ? GetUnchecked(p) : CollisionFlag::All;
}
void Set(Position p, CollisionFlag value);

// Copies `room`'s collision into the grid, with its barricade towers blocked on a level
// that has them; `room` must belong to the grid's level.
void CopyRoomCollision(const game::Room& room);

private:
void MarkBarricades(const game::Room& room);
};

// Pre-validated view into a LevelGrid's cell data. Built by OpenWindow() once
Expand Down
10 changes: 8 additions & 2 deletions tests/runtime/fakes/Unit.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "game/Unit.h"

#include "fakes/GameLoopCollaborators.h"
#include "game/Room.h"

// Minimal Unit fake for GameLoop tests. Only the methods GameLoop::TakeSnapshot
// actually calls (Player, ResolvePtr - drives operator bool, Id, Hp, Mp, Area)
// are implemented - other Unit members exist only as declarations and are not
// linked because no test exercises them.
// are implemented, plus GetRoom, which the pathfinder's FindPath links against -
// other Unit members exist only as declarations and are not linked because no
// test exercises them.

namespace d2bs::game {

Expand Down Expand Up @@ -39,4 +41,8 @@ LevelId Unit::Area() const {
return test::State().areaId;
}

Room Unit::GetRoom() const {
return Room();
}

} // namespace d2bs::game
Loading