Skip to content

Fix crash and performance issues in _dirs command - #209

Open
nschimme wants to merge 14 commits into
masterfrom
fix-dirs-command-crash-15391324769196083821
Open

Fix crash and performance issues in _dirs command#209
nschimme wants to merge 14 commits into
masterfrom
fix-dirs-command-crash-15391324769196083821

Conversation

@nschimme

@nschimme nschimme commented Apr 17, 2026

Copy link
Copy Markdown
Owner

Fixed a crash in the _dirs command caused by a dangling reference and an overly restrictive assertion. Specifically:

  • In MapData::shortestPathSearch, changed thisr from a reference to a copy of the RoomHandle to prevent invalid memory access when the underlying QVector reallocates during a push_back.
  • In ShortestPathEmitter::virt_receiveShortestPath, updated the assertion to allow the current room (index 0) to be a valid search result.
  • Optimized ShortestPathRecipient to take the search tree vector by constant reference, preventing expensive copies and detachment during pathfinding.
    Verified the fixes build and all existing tests pass.

PR created automatically by Jules for task 15391324769196083821 started by @nschimme

Summary by Sourcery

Improve robustness and performance of shortest path search and expose path results in a simpler format.

Bug Fixes:

  • Prevent crashes in shortest path search by avoiding dangling RoomHandle references and handling missing target rooms safely.
  • Allow the origin room to be considered a valid shortest path result in path searches.

Enhancements:

  • Refactor shortest path search to use room IDs and lightweight result objects instead of copying internal search trees.
  • Optimize pathfinding performance by using STL containers, pre-reserving node capacity, and a min-priority queue based on distances.
  • Add QDebug streaming operators for room ID types to improve logging of pathfinding and map-related issues.

Tests:

  • Add a dedicated TestShortestPath test suite covering basic pathfinding, inclusion of the start room, and max-distance cutoffs.
  • Integrate the TestShortestPath executable into the CMake test configuration.

- Fix heap-use-after-free in MapData::shortestPathSearch by ensuring
  room handles are copied, not referenced, before vector modification.
- Optimize shortestPathSearch by passing search node vector by
  const-reference to recipients, avoiding redundant copying.
- Fix assertion failure in ShortestPathEmitter that prevented
  matching the starting room (distance 0).
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai

sourcery-ai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors the shortest path search to use ID-based nodes and explicit result objects, fixes a crash caused by dangling references and over‑strict assertions in the _dirs command, optimizes pathfinding data structures, and adds a dedicated unit test binary for shortest path behavior.

Sequence diagram for updated _dirs shortest path search and emission

sequenceDiagram
    actor User
    participant Parser
    participant MapData
    participant Map
    participant ShortestPathEmitter

    User ->> Parser: enter _dirs command
    Parser ->> MapData: shortestPathSearch(origin, filter, recipient, max_hits, max_dist)
    activate MapData
    MapData ->> Map: getRoomHandle(origin.id)
    Map -->> MapData: RoomHandle origin

    loop Dijkstra_like_search
        MapData ->> Map: getRoomHandle(current_room_id)
        Map -->> MapData: RoomHandle current
        MapData ->> Map: getRoomHandle(neighbor_room_id)
        Map -->> MapData: RoomHandle neighbor
        MapData ->> MapData: compute cost and update sp_nodes
        alt neighbor matches filter
            MapData ->> ShortestPathEmitter: receiveShortestPath(Map map, ShortestPathResult result)
        end
    end
    deactivate MapData

    activate ShortestPathEmitter
    ShortestPathEmitter ->> Map: getRoomHandle(result.id)
    Map -->> ShortestPathEmitter: RoomHandle dest
    ShortestPathEmitter ->> Parser: sendToUser("Distance X: name")
    ShortestPathEmitter ->> Parser: sendToUser("dirs: compressed_path")
    deactivate ShortestPathEmitter

    Parser -->> User: display distance and directions
Loading

Updated class diagram for shortest path result and recipient hierarchy

classDiagram
    class Map
    class RoomId {
        +uint32_t asUint32()
    }
    class ExternalRoomId {
        +uint32_t asUint32()
    }
    class ServerRoomId {
        +uint32_t asUint32()
    }

    class ShortestPathResult {
        +RoomId id
        +double dist
        +vector~ExitDirEnum~ path
    }

    class ShortestPathRecipient {
        <<interface>>
        +~ShortestPathRecipient()
        +void receiveShortestPath(const Map &map, ShortestPathResult result)
        -virtual void virt_receiveShortestPath(const Map &map, ShortestPathResult result)
    }

    class ShortestPathEmitter {
        +~ShortestPathEmitter()
        -void virt_receiveShortestPath(const Map &map, ShortestPathResult result)
        -Parser parser
    }

    class Parser

    ShortestPathEmitter --|> ShortestPathRecipient
    ShortestPathRecipient ..> Map : uses
    ShortestPathRecipient ..> ShortestPathResult : uses
    ShortestPathResult --> RoomId : identifies_room
    Parser ..> ShortestPathEmitter : owns_or_uses
    Map ..> RoomId : identifies_rooms
    ExternalRoomId --|> RoomId
    ServerRoomId --|> RoomId
Loading

File-Level Changes

Change Details Files
Refactor shortest path search algorithm to use ID-based nodes, std::vector, and min-heap priority queue while enforcing distance and visited constraints.
  • Replace QVector-based SPNode storage with std::vector and introduce SPNodeIdx and INVALID_SPNODE_IDX for indexing
  • Store RoomId instead of RoomHandle in SPNode and reconstruct RoomHandle via Map::getRoomHandle during search
  • Use a std::priority_queue configured as a min-heap over (distance, node index) pairs instead of negated distances
  • Track visited rooms via RoomIdSet and short-circuit already-visited nodes and non-existent rooms
  • Respect max_dist before pushing successors and skip generating nodes that would exceed the distance limit
  • Reserve initial capacity for node storage to reduce reallocations
src/mapdata/shortestpath.cpp
Change shortest path result delivery to pass an explicit ShortestPathResult object and the Map by const reference, removing dependence on internal SPNode storage and preventing copies.
  • Introduce ShortestPathResult struct holding target RoomId, distance, and a std::vector representing the path
  • Change ShortestPathRecipient interface to receive (const Map&, ShortestPathResult) instead of QVector and endpoint index
  • Build ShortestPathResult inside MapData::shortestPathSearch by walking SPNode parents and reversing the collected directions
  • Update ShortestPathEmitter to look up the result room name via the Map, iterate over result.path to build direction string, and drop the previous assertion that disallowed endpoint index 0
  • Forward ShortestPathResult via ShortestPathRecipient::receiveShortestPath using std::move to avoid copies
src/mapdata/shortestpath.cpp
src/mapdata/shortestpath.h
src/parser/abstractparser.cpp
Centralize terrain and movement cost constants and reuse them in edge weight computation for clarity and maintainability.
  • Define named constexpr doubles for each terrain type and movement modifier (e.g. COST_INDOORS, COST_WATER, COST_DOOR, COST_DEATHTRAP) in an anonymous namespace
  • Refactor terrain_cost to use XFOREACH_RoomTerrainEnum with a macro to map enum values to the corresponding constants
  • Replace magic numeric literals in getLength with references to the new cost constants
src/mapdata/shortestpath.cpp
Improve debuggability and diagnostics for pathfinding and room identifiers.
  • Include QDebug in roomid implementation and declare/define QDebug operator<< overloads for RoomId, ExternalRoomId, and ServerRoomId
  • Add QDebug usage in ShortestPathEmitter to warn when a shortest path result references a non-existent room id
src/map/roomid.h
src/map/roomid.cpp
src/parser/abstractparser.cpp
Add focused unit tests and build wiring for shortest path behavior, including origin-room matches and max_dist cutoffs.
  • Create TestShortestPath test case with a minimal three-room map graph wired via ExternalRawRoom and Map::fromRooms
  • Implement TestRecipient as a ShortestPathRecipient that stores received ShortestPathResult instances for inspection
  • Verify correct direction sequence (NORTH then EAST) and path length for origin to target room search
  • Validate that the search can return the starting room when it matches the filter and that its path is empty
  • Test max_dist cutoff behavior by limiting total distance and asserting only rooms within the threshold are returned
  • Register the TestShortestPath binary in tests/CMakeLists.txt, linking against existing map, test, and Qt libraries and enabling C++17 flags
tests/TestShortestPath.cpp
tests/TestShortestPath.h
tests/CMakeLists.txt
Minor includes and container cleanup to align with new usage patterns.
  • Add RoomIdSet and standard library headers (algorithm, cstdint, vector) to shortestpath.cpp
  • Remove unused Qt containers and synchronization primitives (QVector, QSet, QBasicMutex) from shortestpath.cpp
  • Include QDebug in abstractparser.cpp to support new warnings
  • Include <unordered_set> in groupwidget.cpp in preparation for or to support hash-based containers
src/mapdata/shortestpath.cpp
src/parser/abstractparser.cpp
src/group/groupwidget.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Changing ShortestPathRecipient::virt_receiveShortestPath to take const QVector<SPNode>& conflicts with the comment and likely behavior in ShortestPathEmitter where spnode is modified; if mutation of SPNode instances is required, consider taking a non-const reference to the vector or redesigning that usage to avoid mutating through a const container.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Changing `ShortestPathRecipient::virt_receiveShortestPath` to take `const QVector<SPNode>&` conflicts with the comment and likely behavior in `ShortestPathEmitter` where `spnode` is modified; if mutation of `SPNode` instances is required, consider taking a non-const reference to the vector or redesigning that usage to avoid mutating through a const container.

## Individual Comments

### Comment 1
<location path="src/parser/abstractparser.cpp" line_range="395-400" />
<code_context>

 private:
-    void virt_receiveShortestPath(QVector<SPNode> spnodes, const int endpoint) final
+    void virt_receiveShortestPath(const QVector<SPNode> &spnodes, const int endpoint) final
     {
-        assert(0 < endpoint && endpoint < spnodes.size());
+        assert(0 <= endpoint && endpoint < spnodes.size());

         // Caution: spnode is modified here.
         const SPNode *spnode = &spnodes[endpoint];
</code_context>
<issue_to_address>
**issue (bug_risk):** Signature now takes a const QVector, but the implementation comment indicates the elements are modified.

If `*spnode` is still mutated, this change will either not compile or force const_casts, which is confusing for callers. Either keep the parameter non-const if mutation is intended, or refactor the implementation to work on a copy so the parameter can safely remain const.
</issue_to_address>

### Comment 2
<location path="src/mapdata/shortestpath.h" line_range="29-32" />
<code_context>

 private:
-    virtual void virt_receiveShortestPath(QVector<SPNode> spnodes, int endpoint) = 0;
+    virtual void virt_receiveShortestPath(const QVector<SPNode> &spnodes, int endpoint) = 0;

 public:
-    void receiveShortestPath(QVector<SPNode> spnodes, const int endpoint)
+    void receiveShortestPath(const QVector<SPNode> &spnodes, const int endpoint)
     {
         virt_receiveShortestPath(spnodes, endpoint);
</code_context>
<issue_to_address>
**question (bug_risk):** Changing from pass-by-value to const-reference tightens lifetime expectations for `spnodes`.

Previously, passing `QVector<SPNode>` by value let implementations own and freely retain or modify their copy. With `const QVector<SPNode>&`, the data is now non-owning and only valid for the duration of the call. Any implementation that stores references/pointers into `spnodes` or assumes it outlives the function may now be unsafe. Please verify existing implementers of this interface don’t rely on longer lifetimes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/parser/abstractparser.cpp Outdated
Comment thread src/mapdata/shortestpath.h Outdated
@codecov

codecov Bot commented Apr 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 83 lines in your changes missing coverage. Please review.
✅ Project coverage is 25.40%. Comparing base (a6c8653) to head (cdf972b).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/mapdata/shortestpath.cpp 0.00% 49 Missing ⚠️
src/mapdata/GenericFind.cpp 0.00% 15 Missing ⚠️
src/parser/abstractparser.cpp 0.00% 11 Missing ⚠️
src/map/roomid.cpp 0.00% 6 Missing ⚠️
src/mapdata/shortestpath.h 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #209      +/-   ##
==========================================
- Coverage   25.40%   25.40%   -0.01%     
==========================================
  Files         519      519              
  Lines       43102    43110       +8     
  Branches     4698     4705       +7     
==========================================
  Hits        10952    10952              
- Misses      32150    32158       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@nschimme

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • Changing ShortestPathRecipient::receiveShortestPath and virt_receiveShortestPath to take const QVector<SPNode> & introduces a lifetime requirement on the caller; consider enforcing this more explicitly (e.g., by passing a value, a shared container, or a view/span type) or documenting that the vector must outlive the recipient callback to avoid future dangling references.
  • In MapData::shortestPathSearch, now that thisr is intentionally a copy, you may want to declare it as const RoomHandle thisr = ...; to make its immutability explicit and prevent accidental modifications that could obscure the reason for copying.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Changing `ShortestPathRecipient::receiveShortestPath` and `virt_receiveShortestPath` to take `const QVector<SPNode> &` introduces a lifetime requirement on the caller; consider enforcing this more explicitly (e.g., by passing a value, a shared container, or a view/span type) or documenting that the vector must outlive the recipient callback to avoid future dangling references.
- In `MapData::shortestPathSearch`, now that `thisr` is intentionally a copy, you may want to declare it as `const RoomHandle thisr = ...;` to make its immutability explicit and prevent accidental modifications that could obscure the reason for copying.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Fix crash in ShortestPathEmitter by allowing matches for the current room
  (relaxing assertion for distance-0 results).
- Optimize shortest path search by using std::vector, std::priority_queue,
  and a flat array for the visited set.
- Modernize ShortestPathRecipient interface to pass results by const ref.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to elements in a reallocating vector.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint >= 0).
- Optimize shortest path search:
  - Replaced QVector/QSet with std::vector and flat uint8_t array.
  - Implemented proper min-heap with std::priority_queue.
  - Updated search logic to skip reporting the origin room (index 0).
- Extract magical cost constants into named constexpr variables.
- Fix -Wsign-conversion and ensure clang-format compliance.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Fix crash in ShortestPathEmitter by relaxing assertion for distance-0
  results, allowing matches for the current room.
- Modernize shortest path search logic:
  - Replaced QVector/QSet with std::vector and flat uint8_t array.
  - Implemented proper min-heap with std::priority_queue.
  - Updated search to intentionally skip reporting the origin room.
- Improved maintainability of movement costs:
  - Extracted costs into named constexpr variables.
  - Refactored terrain_cost() to use the X_CASE macro pattern.
- Ensure -Wsign-conversion and clang-format compliance.
- Add missing <unordered_set> include in groupwidget.cpp.
@nschimme

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The use of visited as a std::vector<uint8_t>(map.getRoomsCount() + 1) keyed by RoomId::asUint32() is a bit opaque—consider adding a brief comment or helper to document the invariant between RoomId values and getRoomsCount() (and why +1 is safe) to prevent future off‑by‑one or out‑of‑range assumptions.
  • There is a lot of casting between int and std::size_t for shortest‑path node indices (e.g., in sp_nodes, endpoint, and parent), which makes the code harder to reason about; consider standardizing on an unsigned index type (or a dedicated alias) and using a sentinel/optional instead of -1 so you can drop the repeated casts and signed/unsigned checks.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The use of `visited` as a `std::vector<uint8_t>(map.getRoomsCount() + 1)` keyed by `RoomId::asUint32()` is a bit opaque—consider adding a brief comment or helper to document the invariant between `RoomId` values and `getRoomsCount()` (and why `+1` is safe) to prevent future off‑by‑one or out‑of‑range assumptions.
- There is a lot of casting between `int` and `std::size_t` for shortest‑path node indices (e.g., in `sp_nodes`, `endpoint`, and `parent`), which makes the code harder to reason about; consider standardizing on an unsigned index type (or a dedicated alias) and using a sentinel/optional instead of `-1` so you can drop the repeated casts and signed/unsigned checks.

## Individual Comments

### Comment 1
<location path="src/mapdata/shortestpath.cpp" line_range="118-127" />
<code_context>
+    std::vector<uint8_t> visited(map.getRoomsCount() + 1, 0);
</code_context>
<issue_to_address>
**issue (bug_risk):** RoomId-to-index mapping for `visited` assumes a tight ID range and can break visited semantics for out-of-range IDs.

This relies on `room_id.asUint32() <= map.getRoomsCount()` (plus the `+1` margin). If `RoomId` values are sparse or can exceed `getRoomsCount()`, any `room_id.asUint32() >= visited.size()` will never be tracked as visited, so those rooms may be re-expanded indefinitely and Dijkstra’s finalization invariant breaks. Unless the tight ID range is guaranteed, consider either sizing `visited` from the maximum internal ID, reverting to a set-based representation, or explicitly handling out-of-range IDs (e.g., early-continue with a clear policy) instead of silently skipping them.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/mapdata/shortestpath.cpp Outdated
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint < size).
- Optimize shortest path search:
  - Switched from breadth-first search to Dijkstra with std::priority_queue.
  - Replaced QVector/QSet with std::vector and the idiomatic RoomIdSet.
  - Updated search logic to skip reporting the origin room (spidx != 0).
- Standardize indices using SPNodeIdx (uint32_t) and INVALID_SPNODE_IDX.
- extract magical cost constants into named constexpr variables.
- Refactor terrain_cost() to use the X_CASE macro pattern.
- Ensure -Wsign-conversion and clang-format compliance.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to elements in a reallocating vector.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint < size).
- Optimize shortest path search:
  - Switched from breadth-first search to Dijkstra with std::priority_queue.
  - Replaced QVector/QSet with std::vector and the idiomatic RoomIdSet.
  - Performed internal path reconstruction to pass results via
    ShortestPathResult, making the recipient interface safer.
  - Updated search logic to skip reporting the origin room (spidx != 0).
- Improve code quality and maintainability:
  - Standardized indices using SPNodeIdx (uint32_t) and INVALID_SPNODE_IDX.
  - Extracted magical cost constants into named constexpr variables.
  - Refactored terrain_cost() to use the X_CASE macro pattern.
- Ensure -Wsign-conversion and clang-format compliance.
- Add unit tests for shortestPathSearch in TestShortestPath.cpp.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint < size).
- Optimize shortest path search:
  - Switched from breadth-first search to Dijkstra with std::priority_queue.
  - Replaced QVector/QSet with std::vector and the idiomatic RoomIdSet.
  - Performed internal path reconstruction to pass results via
    ShortestPathResult, making the recipient interface safer and more performant.
  - Updated search logic to skip reporting the origin room (spidx != 0).
- Improve code quality and maintainability:
  - Standardized indices using SPNodeIdx (uint32_t) and INVALID_SPNODE_IDX.
  - Extracted magical cost constants into named constexpr variables.
  - Refactored terrain_cost() to use the X_CASE macro pattern.
- Ensure -Wsign-conversion and clang-format compliance.
- Add comprehensive unit tests for shortestPathSearch in TestShortestPath.cpp.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint < size).
- Optimize shortest path search:
  - Switched from breadth-first search to Dijkstra with std::priority_queue.
  - Replaced QVector/QSet with std::vector and the idiomatic RoomIdSet.
  - Performed internal path reconstruction to pass results via
    ShortestPathResult, making the recipient interface safer and more performant.
- Improve code quality and maintainability:
  - Standardized indices using SPNodeIdx (uint32_t) and INVALID_SPNODE_IDX.
  - Extracted magical cost constants into named constexpr variables.
  - Refactored terrain_cost() to use the X_CASE macro pattern.
- Ensure -Wsign-conversion and clang-format compliance.
- Add comprehensive unit tests for shortestPathSearch in TestShortestPath.cpp.
- Add missing <unordered_set> include in groupwidget.cpp.
@nschimme

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • Consider using std::size_t (or the container’s size_type) for SPNodeIdx instead of std::uint32_t, or at least adding a safeguard, to avoid potential truncation/overflow if the number of path nodes grows beyond 2^32−1.
  • In MapData::shortestPathSearch, you now duplicate path reconstruction logic that previously lived in the recipient; if future consumers need different path projections (e.g., including nodes, not just directions), it might be worth centralizing or exposing a helper to avoid reimplementing the reconstruction in multiple places.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider using `std::size_t` (or the container’s `size_type`) for `SPNodeIdx` instead of `std::uint32_t`, or at least adding a safeguard, to avoid potential truncation/overflow if the number of path nodes grows beyond 2^32−1.
- In `MapData::shortestPathSearch`, you now duplicate path reconstruction logic that previously lived in the recipient; if future consumers need different path projections (e.g., including nodes, not just directions), it might be worth centralizing or exposing a helper to avoid reimplementing the reconstruction in multiple places.

## Individual Comments

### Comment 1
<location path="src/parser/abstractparser.cpp" line_range="395-398" />
<code_context>

 private:
-    void virt_receiveShortestPath(QVector<SPNode> spnodes, const int endpoint) final
+    void virt_receiveShortestPath(const Map &map, ShortestPathResult result) final
     {
-        assert(0 < endpoint && endpoint < spnodes.size());
+        const auto room = map.getRoomHandle(result.id);
+        const auto name = room ? room.getName() : RoomName{};

-        // Caution: spnode is modified here.
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing a missing room silently could make underlying data issues harder to detect.

Since virt_receiveShortestPath now falls back to an empty RoomName when map.getRoomHandle(result.id) fails, any missing room will appear as a "normal" case. Because the search code should only emit results for existing rooms, hitting this path likely indicates a data inconsistency. Consider asserting or logging a warning instead of silently using an empty name, so these issues are visible during development and debugging.

Suggested implementation:

```cpp
    void virt_receiveShortestPath(const Map &map, ShortestPathResult result) final
    {
        const auto room = map.getRoomHandle(result.id);
        if (!room) {
            qWarning() << "ShortestPathEmitter::virt_receiveShortestPath received result for non-existent room id"
                       << result.id;
            return;
        }
        const auto name = room.getName();

        parser.sendToUser(SendToUserSourceEnum::FromMMapper,
                          "Distance " + std::to_string(result.dist) + ": " + name.toStdStringUtf8()
                              + "\n");

```

If `qWarning` is not already used in this file, ensure the appropriate Qt debug header is included, e.g. `#include <QDebug>`, near the top of `src/parser/abstractparser.cpp`. This will make the missing-room cases visible during development and debugging while avoiding undefined behavior in release builds.
</issue_to_address>

### Comment 2
<location path="tests/TestShortestPath.cpp" line_range="124-129" />
<code_context>
+    QVERIFY(optFilter.has_value());
+    TestRecipient recipient;
+
+    MapData::shortestPathSearch(r1_handle, recipient, *optFilter, 1, 0);
+
+    QCOMPARE(recipient.results.size(), 1ULL);
+    QCOMPARE(recipient.results[0].path.size(), 2ULL);
+    QCOMPARE(recipient.results[0].path[0], ExitDirEnum::NORTH);
+    QCOMPARE(recipient.results[0].path[1], ExitDirEnum::EAST);
+
+    // Test including start room
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test covering `max_dist` cutoff behavior in `shortestPathSearch`

The updated code adds explicit `max_dist` handling, but this test always uses `max_dist = 0` (no limit), so that code path isn’t covered. Please add a case with a finite `max_dist` that permits the first step but not the second (or vice versa), and assert that farther rooms are excluded. This will protect against regressions in cost accumulation and the `max_dist` early-return behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/parser/abstractparser.cpp Outdated
Comment thread tests/TestShortestPath.cpp Outdated
Comment on lines +124 to +129
MapData::shortestPathSearch(r1_handle, recipient, *optFilter, 1, 0);

QCOMPARE(recipient.results.size(), 1ULL);
QCOMPARE(recipient.results[0].path.size(), 2ULL);
QCOMPARE(recipient.results[0].path[0], ExitDirEnum::NORTH);
QCOMPARE(recipient.results[0].path[1], ExitDirEnum::EAST);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add a test covering max_dist cutoff behavior in shortestPathSearch

The updated code adds explicit max_dist handling, but this test always uses max_dist = 0 (no limit), so that code path isn’t covered. Please add a case with a finite max_dist that permits the first step but not the second (or vice versa), and assert that farther rooms are excluded. This will protect against regressions in cost accumulation and the max_dist early-return behavior.

- Fix heap-use-after-free in MapData::shortestPathSearch by using RoomId
  instead of references to QVector elements that can be invalidated.
- Resolve assertion failure in ShortestPathEmitter by allowing results
  for the current room (relaxed assertion to endpoint < size).
- Optimize shortest path search:
  - Switched from breadth-first search to Dijkstra with std::priority_queue.
  - Replaced QVector/QSet with std::vector and the idiomatic RoomIdSet.
  - Performed internal path reconstruction to pass results via
    ShortestPathResult, making the recipient interface safer and more performant.
  - Explicitly handle max_dist cutoff and ensure correct cost accumulation.
- Improve code quality and maintainability:
  - Standardized indices using std::size_t and INVALID_SPNODE_IDX.
  - Extracted magical cost constants into named constexpr variables.
  - Refactored terrain_cost() to use the X_CASE macro pattern.
  - Added warning logging to ShortestPathEmitter for data inconsistencies.
- Ensure -Wsign-conversion and clang-format compliance.
- Add comprehensive unit tests (including max_dist) in TestShortestPath.cpp.
- Add missing <unordered_set> include in groupwidget.cpp.
Resolved a heap-use-after-free in MapData::shortestPathSearch caused by
holding a reference to a QVector element while the vector reallocated.

Changes:
- Re-implemented shortest path search using Dijkstra's algorithm with
  std::priority_queue (min-heap) for efficiency.
- Decoupled path reconstruction from the recipient interface; search now
  returns a self-contained ShortestPathResult.
- Standardized use of RoomId in search nodes to minimize memory footprint
  and avoid dangling handles.
- Replaced QVector/QSet with std::vector and RoomIdSet for core search
  logic.
- Fixed a crash in ShortestPathEmitter assertion when matches were found
  in the starting room (distance 0).
- Included <QDebug> in roomid.h to fix build failures across platforms
  caused by incomplete type deduction in Qt 6 operator overloads.
- Added comprehensive regression tests in tests/TestShortestPath.cpp.

Fixes MUME#519
- Resolve heap-use-after-free in MapData::shortestPathSearch by avoiding dangling references during vector reallocation.
- Implement Dijkstra's algorithm with std::priority_queue for robust pathfinding.
- Include the starting room in search results if filter criteria are met.
- Add regression tests in tests/TestShortestPath.cpp.
- Fix Qt 6 build issues by including <QDebug> in src/map/roomid.h.
- Fix missing <unordered_set> include in src/group/groupwidget.cpp.
@nschimme

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@SourceryAI SourceryAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In MapData::shortestPathSearch, exits pointing to non-existent rooms are now silently skipped; consider keeping at least a qWarning() here to preserve diagnostics for corrupted or out-of-sync maps while still avoiding crashes.
  • The fixed INITIAL_NODES_CAPACITY of 1024 for the shortest-path node vector may be suboptimal for very small or very large maps; you might want to derive this from map size or make it configurable to avoid over-allocation or repeated growth.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MapData::shortestPathSearch`, exits pointing to non-existent rooms are now silently skipped; consider keeping at least a `qWarning()` here to preserve diagnostics for corrupted or out-of-sync maps while still avoiding crashes.
- The fixed `INITIAL_NODES_CAPACITY` of 1024 for the shortest-path node vector may be suboptimal for very small or very large maps; you might want to derive this from map size or make it configurable to avoid over-allocation or repeated growth.

## Individual Comments

### Comment 1
<location path="tests/TestShortestPath.cpp" line_range="19-28" />
<code_context>
+#include <QDebug>
+
 std::ostream &operator<<(std::ostream &os, const RoomId id)
 {
     return os << "RoomId(" << id.value() << ")";
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert on the computed distances to validate movement cost handling, not just directions

This block depends on the indoor terrain cost (0.75 per step) and the new cost constants, but the test only verifies the direction sequence. Please also assert the computed distance (e.g. `QCOMPARE(recipient.results[0].dist, 1.5);` or a fuzzy FP check) so regressions in `terrain_cost` or `getLength` are caught.
</issue_to_address>

Hi @nschimme! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/TestShortestPath.cpp Outdated
Comment on lines +19 to +28
{
return Abbrev("terrain", 1);
}
Abbrev getParserCommandName(RoomLightEnum)
{
return Abbrev("light", 1);
}
Abbrev getParserCommandName(RoomRidableEnum)
{
return Abbrev("ridable", 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Also assert on the computed distances to validate movement cost handling, not just directions

This block depends on the indoor terrain cost (0.75 per step) and the new cost constants, but the test only verifies the direction sequence. Please also assert the computed distance (e.g. QCOMPARE(recipient.results[0].dist, 1.5); or a fuzzy FP check) so regressions in terrain_cost or getLength are caught.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In MapData::shortestPathSearch, when map.getRoomHandle(nextrId) fails the code now silently continues; consider restoring at least a qWarning (without asserting) so unexpected broken exits remain diagnosable at runtime instead of failing quietly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MapData::shortestPathSearch`, when `map.getRoomHandle(nextrId)` fails the code now silently `continue`s; consider restoring at least a `qWarning` (without asserting) so unexpected broken exits remain diagnosable at runtime instead of failing quietly.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

- Resolve heap-use-after-free in MapData::shortestPathSearch by refactoring to a safe Dijkstra implementation using indices and value-copies.
- Support including the starting room in search results by relaxing emitter constraints.
- Modernize terrain cost logic and search node management.
- Address build failures on non-Linux platforms by ensuring QDebug is a complete type in roomid.h and providing out-of-line operator definitions.
- Add missing <unordered_set> include in groupwidget.cpp.
- Fix heap-use-after-free in shortestPathSearch by refactoring to Dijkstra with value-copies.
- Parallelize room filtering in genericFind using thread_utils to reduce latency.
- Integrate parallel pre-filtering into shortestPathSearch to speed up target identification.
- Optimize path reconstruction to avoid std::reverse by pre-counting and back-filling.
- Add DECL_TIMER performance instrumentation.
- Support including the starting room in results ("dirs: (here)").
- Fix build issues on non-Linux platforms for RoomId and missing includes.
@nschimme
nschimme force-pushed the master branch 2 times, most recently from e8139f3 to c119262 Compare April 20, 2026 18:27
@nschimme
nschimme force-pushed the master branch 2 times, most recently from ae664f2 to bcd8fca Compare May 21, 2026 23:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants