diff --git a/src/runtime/components/navigation/Pathfinder.cpp b/src/runtime/components/navigation/Pathfinder.cpp index d32f60a..2313eee 100644 --- a/src/runtime/components/navigation/Pathfinder.cpp +++ b/src/runtime/components/navigation/Pathfinder.cpp @@ -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 { @@ -161,39 +162,45 @@ std::vector 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(std::min(roomH, collData.size() / roomW)); - const auto rows = static_cast(std::min(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; @@ -978,12 +985,41 @@ std::vector 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 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 FindPath(const PathRequest& request) { @@ -996,6 +1032,8 @@ std::vector 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, diff --git a/src/runtime/components/navigation/Pathfinder.h b/src/runtime/components/navigation/Pathfinder.h index 8a41cf6..1d4c6e4 100644 --- a/src/runtime/components/navigation/Pathfinder.h +++ b/src/runtime/components/navigation/Pathfinder.h @@ -14,6 +14,7 @@ namespace d2bs::game { class Level; +class Room; } // namespace d2bs::game namespace d2bs::runtime::navigation { @@ -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 diff --git a/tests/runtime/fakes/Unit.cpp b/tests/runtime/fakes/Unit.cpp index 77d9ed8..b43868b 100644 --- a/tests/runtime/fakes/Unit.cpp +++ b/tests/runtime/fakes/Unit.cpp @@ -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 { @@ -39,4 +41,8 @@ LevelId Unit::Area() const { return test::State().areaId; } +Room Unit::GetRoom() const { + return Room(); +} + } // namespace d2bs::game