Skip to content

Save and load to yml#22

Merged
deanblackborough merged 10 commits intomainfrom
save-and-load-to-yml
Apr 19, 2026
Merged

Save and load to yml#22
deanblackborough merged 10 commits intomainfrom
save-and-load-to-yml

Conversation

@deanblackborough
Copy link
Copy Markdown
Owner

@deanblackborough deanblackborough commented Apr 19, 2026

Summary by CodeRabbit

  • New Features

    • Scene persistence: New/Save/Load scene actions (YAML); restores objects, camera, grid and editor options; New Scene resets to defaults.
    • Scene management: New scene reset/restore flows and explicit New Scene action.
  • UI

    • "Add Block" button label.
    • File operation status messages displayed in the UI.
  • Documentation

    • README updated with scene persistence details, usage, and build instructions; Yaml‑cpp added to tech stack.
  • Chores

    • Build system updated to declare YAML support and include package manifest.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds YAML-based scene persistence and UI commands for New/Save/Load of the sandbox scene. A new serialization unit implements scene save/load (scene metadata, camera, grid, options, objects) and integrates with SandboxScene::save_to_file / load_from_file. GameObjectManager gains add_loaded_object, set_selected_id, set_next_id and next_id APIs. SandboxScene initialisation is refactored into new_scene, reset_runtime_state and restore_defaults. UI now exposes File → New/Save/Load with status feedback. Build, vcpkg manifest, CMake and Windows CI were updated to locate and link yaml-cpp.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately reflects the primary objective of the changeset: implementing YAML-based scene persistence (saving and loading).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch save-and-load-to-yml

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Apr 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1519b6e6-2b02-4708-bf6c-70b0543aa81c

📥 Commits

Reviewing files that changed from the base of the PR and between 4f6565d and fb35057.

📒 Files selected for processing (10)
  • CMakeLists.txt
  • README.md
  • src/prune/scene/game_object_manager.cpp
  • src/prune/scene/game_object_manager.hpp
  • src/prune/scene/sandbox_scene.cpp
  • src/prune/scene/sandbox_scene.hpp
  • src/prune/scene/sandbox_scene_serialization.cpp
  • src/prune/tooling/outliner.cpp
  • src/prune/tooling/ui.cpp
  • src/prune/tooling/ui.hpp

Comment thread README.md
Comment on lines 166 to +185
## Next
- Camera and limitless world space foundation
### Foundation
- Camera and limitless world space
- Editor camera vs player camera separation
- Object types (player, blocks, platforms, hazards, etc.)
- Static sprites

### Editing tools
- Gizmos for object movement
- Save and load
- Animated sprites
- Editor camera
- Player camera
- Gizmos for scaling the selected object

### Core systems
- Undo/redo system
- ENTT at some point
- Multi-scene support, game type per scene (top-down, platformer, etc)
- Improved save and load (v2)

### Content
- Animated sprites

### Architecture
- Multi-scene support (scene types: platformer, top-down, etc.)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Give these headings a blank line each side.

Markdownlint is right here: the ##/### headings in Next are packed straight against neighbouring content, so this section will keep failing MD022 until you separate them.

💡 Minimal tidy-up
 ## Next
+
 ### Foundation
+
 - Camera and limitless world space
 - Editor camera vs player camera separation
 - Object types (player, blocks, platforms, hazards, etc.)
 - Static sprites
+
 ### Editing tools
+
 - Gizmos for object movement
 - Gizmos for scaling the selected object
+
 ### Core systems
+
 - Undo/redo system
 - Improved save and load (v2)
+
 ### Content
+
 - Animated sprites
+
 ### Architecture
+
 - Multi-scene support (scene types: platformer, top-down, etc.)
 - ENTT (ECS integration)
🧰 Tools
🪛 LanguageTool

[uncategorized] ~177-~177: It seems likely that a singular genitive (’s) apostrophe is missing.
Context: ...r scaling the selected object ### Core systems - Undo/redo system - Improved save and ...

(AI_HYDRA_LEO_APOSTROPHE_S_XS)

🪛 markdownlint-cli2 (0.22.0)

[warning] 166-166: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 167-167: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 167-167: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 173-173: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 177-177: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 181-181: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 184-184: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

Comment on lines +52 to +55
void GameObjectManager::set_next_id(GameObjectId next_id) noexcept
{
m_next_id = std::max<GameObjectId>(1, next_id);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify std::max usage and direct include coverage in the touched files.
rg -n 'std::max|#include <algorithm>' src/prune/scene/game_object_manager.cpp src/prune/scene/game_object_manager.hpp

Repository: deanblackborough/Prune

Length of output: 164


🏁 Script executed:

head -30 src/prune/scene/game_object_manager.cpp

Repository: deanblackborough/Prune

Length of output: 765


🏁 Script executed:

head -30 src/prune/scene/game_object_manager.hpp

Repository: deanblackborough/Prune

Length of output: 1034


🏁 Script executed:

head -20 src/prune/scene/game_object.hpp

Repository: deanblackborough/Prune

Length of output: 387


🏁 Script executed:

# Check if <vector> transitively provides std::max by inspecting a minimal test
cat > /tmp/test_max.cpp << 'EOF'
`#include` <vector>
int main() {
    int x = std::max(1, 2);
    return x;
}
EOF
# Try to compile with standard library only (this will fail if <algorithm> is truly needed)
echo "Note: checking if std::max is available through <vector> alone..."

Repository: deanblackborough/Prune

Length of output: 131


🏁 Script executed:

fd -t f "game_object_manager" src/prune/

Repository: deanblackborough/Prune

Length of output: 145


Add <algorithm> for std::max usage.

Line 54 uses std::max<GameObjectId>, but the file doesn't include <algorithm>. It's relying on something it hasn't explicitly asked for—bad habit in C++. You'll find your code breaks on stricter toolchains or when header inclusion order changes. Make it explicit:

Proposed fix
 `#include` "prune/scene/game_object_manager.hpp"
+#include <algorithm>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void GameObjectManager::set_next_id(GameObjectId next_id) noexcept
{
m_next_id = std::max<GameObjectId>(1, next_id);
}
`#include` "prune/scene/game_object_manager.hpp"
`#include` <algorithm>
🧰 Tools
🪛 Cppcheck (2.20.0)

[style] 52-52: The function 'set_next_id' is never used.

(unusedFunction)

Comment on lines +174 to +181
std::ofstream output{ std::string(path) };
if (!output.is_open()) {
error = "Could not open save file for writing.";
return false;
}

output << root;
return true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Check the write, not just the open.

is_open() only proves the handle was created. If the disk fills up or the flush fails after output << root, this still returns true and the editor will think the scene was saved when it was not.

💡 Tighten the save-path error handling
             std::ofstream output{ std::string(path) };
             if (!output.is_open()) {
                 error = "Could not open save file for writing.";
                 return false;
             }

             output << root;
+            output.flush();
+            if (!output) {
+                error = "Failed to write save file.";
+                return false;
+            }
             return true;
🧰 Tools
🪛 Clang (14.0.6)

[warning] 174-174: variable 'output' is not initialized

(cppcoreguidelines-init-variables)


[warning] 177-177: redundant boolean literal in conditional return statement

(readability-simplify-boolean-expr)

Comment on lines +209 to +260
reset_runtime_state();

GameObjectId loaded_next_id = 1;
GameObjectId loaded_player_id = kInvalidGameObjectId;
GameObjectId loaded_selected_id = kInvalidGameObjectId;

if (!read_required_uint(scene, "next_object_id", loaded_next_id)) {
error = "scene.next_object_id is missing.";
return false;
}

if (!read_required_uint(scene, "player_id", loaded_player_id)) {
error = "scene.player_id is missing.";
return false;
}

if (scene["selected_object_id"]) {
loaded_selected_id = scene["selected_object_id"].as<GameObjectId>();
}

if (!read_required_float(camera, "x", m_camera.x) ||
!read_required_float(camera, "y", m_camera.y)) {
error = "camera is incomplete.";
return false;
}

if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
!read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
!read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
!read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
!read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
error = "grid is incomplete.";
return false;
}

if (!read_required_bool(options, "highlight_selected", m_scene_options.highlight_selected)) {
error = "options.highlight_selected is missing.";
return false;
}

int player_count = 0;

for (const auto& entry : objects) {
GameObject object{};
if (!load_object_from_node(entry, object, error)) {
return false;
}

if (!m_objects.add_loaded_object(object)) {
error = "Failed to restore object. Duplicate or invalid id.";
return false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Make the load path transactional.

Right now you clear runtime state before the file has fully validated, then write camera/grid/options straight into the live scene as you parse. A malformed YAML can therefore wipe or partially replace the current scene and still return false, which is a nasty editor data-loss path. Parse into temporaries first, then commit once the whole file is known-good.

💡 Safer shape for the loader
-            reset_runtime_state();
-
             GameObjectId loaded_next_id = 1;
             GameObjectId loaded_player_id = kInvalidGameObjectId;
             GameObjectId loaded_selected_id = kInvalidGameObjectId;
+            decltype(m_camera) loaded_camera{};
+            decltype(m_grid_options) loaded_grid{};
+            decltype(m_scene_options) loaded_options{};
+            std::vector<GameObject> loaded_objects;
...
-            if (!read_required_float(camera, "x", m_camera.x) ||
-                !read_required_float(camera, "y", m_camera.y)) {
+            if (!read_required_float(camera, "x", loaded_camera.x) ||
+                !read_required_float(camera, "y", loaded_camera.y)) {
                 error = "camera is incomplete.";
                 return false;
             }
...
-            if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
-                !read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
-                !read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
-                !read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
-                !read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
+            if (!read_required_bool(grid, "show_grid", loaded_grid.show_grid) ||
+                !read_required_bool(grid, "snap_to_grid", loaded_grid.snap_to_grid) ||
+                !read_required_int(grid, "grid_size", loaded_grid.grid_size) ||
+                !read_required_int(grid, "nudge_step", loaded_grid.nudge_step) ||
+                !read_required_int(grid, "shift_nudge_steps", loaded_grid.shift_nudge_steps)) {
                 error = "grid is incomplete.";
                 return false;
             }
...
-                if (!m_objects.add_loaded_object(object)) {
-                    error = "Failed to restore object. Duplicate or invalid id.";
-                    return false;
-                }
+                loaded_objects.push_back(object);
...
+            reset_runtime_state();
+            m_camera = loaded_camera;
+            m_grid_options = loaded_grid;
+            m_scene_options = loaded_options;
+            for (const auto& object : loaded_objects) {
+                if (!m_objects.add_loaded_object(object)) {
+                    error = "Failed to restore object. Duplicate or invalid id.";
+                    return false;
+                }
+            }
🧰 Tools
🪛 Clang (14.0.6)

[warning] 211-211: variable 'loaded_next_id' is not initialized

(cppcoreguidelines-init-variables)


[warning] 212-212: variable 'loaded_player_id' is not initialized

(cppcoreguidelines-init-variables)


[warning] 213-213: variable 'loaded_selected_id' is not initialized

(cppcoreguidelines-init-variables)

Comment on lines +215 to +290
if (!read_required_uint(scene, "next_object_id", loaded_next_id)) {
error = "scene.next_object_id is missing.";
return false;
}

if (!read_required_uint(scene, "player_id", loaded_player_id)) {
error = "scene.player_id is missing.";
return false;
}

if (scene["selected_object_id"]) {
loaded_selected_id = scene["selected_object_id"].as<GameObjectId>();
}

if (!read_required_float(camera, "x", m_camera.x) ||
!read_required_float(camera, "y", m_camera.y)) {
error = "camera is incomplete.";
return false;
}

if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
!read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
!read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
!read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
!read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
error = "grid is incomplete.";
return false;
}

if (!read_required_bool(options, "highlight_selected", m_scene_options.highlight_selected)) {
error = "options.highlight_selected is missing.";
return false;
}

int player_count = 0;

for (const auto& entry : objects) {
GameObject object{};
if (!load_object_from_node(entry, object, error)) {
return false;
}

if (!m_objects.add_loaded_object(object)) {
error = "Failed to restore object. Duplicate or invalid id.";
return false;
}

if (object.is_player) {
++player_count;
}
}

if (m_objects.empty()) {
error = "Save file contains no objects.";
return false;
}

if (player_count != 1) {
error = "Save file must contain exactly one player object.";
return false;
}

m_player_id = loaded_player_id;
GameObject* player = m_objects.get_by_id(m_player_id);

if (!player) {
error = "Saved player_id does not exist.";
return false;
}

if (!player->is_player) {
error = "Saved player_id does not point to a player object.";
return false;
}

m_objects.set_next_id(loaded_next_id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject next_object_id values that move backwards.

The loader trusts scene.next_object_id blindly. If a hand-edited file sets it to an existing ID, or to the invalid sentinel, the next spawned object can collide with an already loaded one and break the manager later. Sanity-check it against the highest loaded object ID before calling set_next_id().

💡 Validate the loaded sequence point
             int player_count = 0;
+            GameObjectId max_loaded_id = kInvalidGameObjectId;

             for (const auto& entry : objects) {
                 GameObject object{};
                 if (!load_object_from_node(entry, object, error)) {
                     return false;
@@
                 if (object.is_player) {
                     ++player_count;
                 }
+
+                if (object.id > max_loaded_id) {
+                    max_loaded_id = object.id;
+                }
             }
@@
+            if (loaded_next_id == kInvalidGameObjectId || loaded_next_id <= max_loaded_id) {
+                error = "scene.next_object_id must be greater than every loaded object id.";
+                return false;
+            }
+
             m_objects.set_next_id(loaded_next_id);
🧰 Tools
🪛 Clang (14.0.6)

[warning] 278-278: variable 'player' is not initialized

(cppcoreguidelines-init-variables)


[warning] 280-280: implicit conversion 'prune::GameObject *' -> bool

(readability-implicit-bool-conversion)

Comment on lines +225 to +298
if (scene["selected_object_id"]) {
loaded_selected_id = scene["selected_object_id"].as<GameObjectId>();
}

if (!read_required_float(camera, "x", m_camera.x) ||
!read_required_float(camera, "y", m_camera.y)) {
error = "camera is incomplete.";
return false;
}

if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
!read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
!read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
!read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
!read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
error = "grid is incomplete.";
return false;
}

if (!read_required_bool(options, "highlight_selected", m_scene_options.highlight_selected)) {
error = "options.highlight_selected is missing.";
return false;
}

int player_count = 0;

for (const auto& entry : objects) {
GameObject object{};
if (!load_object_from_node(entry, object, error)) {
return false;
}

if (!m_objects.add_loaded_object(object)) {
error = "Failed to restore object. Duplicate or invalid id.";
return false;
}

if (object.is_player) {
++player_count;
}
}

if (m_objects.empty()) {
error = "Save file contains no objects.";
return false;
}

if (player_count != 1) {
error = "Save file must contain exactly one player object.";
return false;
}

m_player_id = loaded_player_id;
GameObject* player = m_objects.get_by_id(m_player_id);

if (!player) {
error = "Saved player_id does not exist.";
return false;
}

if (!player->is_player) {
error = "Saved player_id does not point to a player object.";
return false;
}

m_objects.set_next_id(loaded_next_id);

if (loaded_selected_id != kInvalidGameObjectId &&
m_objects.get_by_id(loaded_selected_id) != nullptr) {
m_objects.set_selected_id(loaded_selected_id);
}
else {
m_objects.set_selected_id(m_player_id);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Preserve “no selection” on round-trip.

Save omits selected_object_id when nothing is selected, but load currently falls back to the player. That means a valid save/load cycle changes editor state instead of restoring it. Only fall back when the file carries a stale ID; if the key is absent, clear selection explicitly.

💡 Restore the optional field faithfully
-            if (loaded_selected_id != kInvalidGameObjectId &&
-                m_objects.get_by_id(loaded_selected_id) != nullptr) {
+            if (!scene["selected_object_id"]) {
+                m_objects.set_selected_id(kInvalidGameObjectId);
+            }
+            else if (loaded_selected_id != kInvalidGameObjectId &&
+                     m_objects.get_by_id(loaded_selected_id) != nullptr) {
                 m_objects.set_selected_id(loaded_selected_id);
             }
             else {
                 m_objects.set_selected_id(m_player_id);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (scene["selected_object_id"]) {
loaded_selected_id = scene["selected_object_id"].as<GameObjectId>();
}
if (!read_required_float(camera, "x", m_camera.x) ||
!read_required_float(camera, "y", m_camera.y)) {
error = "camera is incomplete.";
return false;
}
if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
!read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
!read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
!read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
!read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
error = "grid is incomplete.";
return false;
}
if (!read_required_bool(options, "highlight_selected", m_scene_options.highlight_selected)) {
error = "options.highlight_selected is missing.";
return false;
}
int player_count = 0;
for (const auto& entry : objects) {
GameObject object{};
if (!load_object_from_node(entry, object, error)) {
return false;
}
if (!m_objects.add_loaded_object(object)) {
error = "Failed to restore object. Duplicate or invalid id.";
return false;
}
if (object.is_player) {
++player_count;
}
}
if (m_objects.empty()) {
error = "Save file contains no objects.";
return false;
}
if (player_count != 1) {
error = "Save file must contain exactly one player object.";
return false;
}
m_player_id = loaded_player_id;
GameObject* player = m_objects.get_by_id(m_player_id);
if (!player) {
error = "Saved player_id does not exist.";
return false;
}
if (!player->is_player) {
error = "Saved player_id does not point to a player object.";
return false;
}
m_objects.set_next_id(loaded_next_id);
if (loaded_selected_id != kInvalidGameObjectId &&
m_objects.get_by_id(loaded_selected_id) != nullptr) {
m_objects.set_selected_id(loaded_selected_id);
}
else {
m_objects.set_selected_id(m_player_id);
}
if (scene["selected_object_id"]) {
loaded_selected_id = scene["selected_object_id"].as<GameObjectId>();
}
if (!read_required_float(camera, "x", m_camera.x) ||
!read_required_float(camera, "y", m_camera.y)) {
error = "camera is incomplete.";
return false;
}
if (!read_required_bool(grid, "show_grid", m_grid_options.show_grid) ||
!read_required_bool(grid, "snap_to_grid", m_grid_options.snap_to_grid) ||
!read_required_int(grid, "grid_size", m_grid_options.grid_size) ||
!read_required_int(grid, "nudge_step", m_grid_options.nudge_step) ||
!read_required_int(grid, "shift_nudge_steps", m_grid_options.shift_nudge_steps)) {
error = "grid is incomplete.";
return false;
}
if (!read_required_bool(options, "highlight_selected", m_scene_options.highlight_selected)) {
error = "options.highlight_selected is missing.";
return false;
}
int player_count = 0;
for (const auto& entry : objects) {
GameObject object{};
if (!load_object_from_node(entry, object, error)) {
return false;
}
if (!m_objects.add_loaded_object(object)) {
error = "Failed to restore object. Duplicate or invalid id.";
return false;
}
if (object.is_player) {
+player_count;
}
}
if (m_objects.empty()) {
error = "Save file contains no objects.";
return false;
}
if (player_count != 1) {
error = "Save file must contain exactly one player object.";
return false;
}
m_player_id = loaded_player_id;
GameObject* player = m_objects.get_by_id(m_player_id);
if (!player) {
error = "Saved player_id does not exist.";
return false;
}
if (!player->is_player) {
error = "Saved player_id does not point to a player object.";
return false;
}
m_objects.set_next_id(loaded_next_id);
if (!scene["selected_object_id"]) {
m_objects.set_selected_id(kInvalidGameObjectId);
}
else if (loaded_selected_id != kInvalidGameObjectId &&
m_objects.get_by_id(loaded_selected_id) != nullptr) {
m_objects.set_selected_id(loaded_selected_id);
}
else {
m_objects.set_selected_id(m_player_id);
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 278-278: variable 'player' is not initialized

(cppcoreguidelines-init-variables)


[warning] 280-280: implicit conversion 'prune::GameObject *' -> bool

(readability-implicit-bool-conversion)


[warning] 292-292: if with identical then and else branches

(bugprone-branch-clone)


[note] 296-296: else branch starts here

(clang)

Comment on lines +47 to +48
[[nodiscard]] bool save_to_file(std::string_view path, std::string& error) const;
[[nodiscard]] bool load_from_file(std::string_view path, std::string& error);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify string types are used and whether matching standard headers are present.
rg -n 'std::string_view|std::string|#include <string>|#include <string_view>' src/prune/scene/sandbox_scene.hpp

Repository: deanblackborough/Prune

Length of output: 247


🏁 Script executed:

#!/bin/bash
# Get the complete include section at the top of the file
head -50 src/prune/scene/sandbox_scene.hpp | cat -n

Repository: deanblackborough/Prune

Length of output: 1626


Right, listen—this header needs to be self-contained. Always.

You're declaring methods that use std::string_view and std::string on lines 47-48, but you haven't included the headers for those types. Yes, it might compile because something else is pulling those in transitively (perhaps SDL2 or one of your project headers), but relying on that is a recipe for brittle code. In a game engine, these kinds of hidden dependencies will bite you later when you refactor includes.

Add #include <string> and #include <string_view> explicitly at the top of this file. Header files should always be self-sufficient—make it a habit.

Suggested fix
 `#include` <array>
+#include <string>
+#include <string_view>
🧰 Tools
🪛 Clang (14.0.6)

[warning] 47-47: use a trailing return type for this function

(modernize-use-trailing-return-type)


[warning] 48-48: use a trailing return type for this function

(modernize-use-trailing-return-type)

Comment thread src/prune/tooling/ui.cpp
Comment on lines +46 to +53
if (!m_file_status.empty()) {
ImGui::Separator();
if (m_file_status_is_error) {
ImGui::TextWrapped("Status: %s", m_file_status.c_str());
}
else {
ImGui::TextWrapped("Status: %s", m_file_status.c_str());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the status-rendering branch to confirm both branches are identical.
rg -n -A8 -B3 'if \(m_file_status_is_error\)' src/prune/tooling/ui.cpp

Repository: deanblackborough/Prune

Length of output: 524


The m_file_status_is_error branch isn't doing anything useful right now.

You've got identical code in both the if and else branches—lines 49 and 52 render exactly the same thing. So the error flag isn't actually having any effect. This looks like an incomplete feature: you probably wanted to apply different styling (like a warning colour or icon) when there's an error, but left it unfinished.

Either wire up the error styling properly, or just collapse it into a single ImGui::TextWrapped() call for now to keep the code clean.

Quick tidy-up
                 if (!m_file_status.empty()) {
                     ImGui::Separator();
-                    if (m_file_status_is_error) {
-                        ImGui::TextWrapped("Status: %s", m_file_status.c_str());
-                    }
-                    else {
-                        ImGui::TextWrapped("Status: %s", m_file_status.c_str());
-                    }
+                    ImGui::TextWrapped("Status: %s", m_file_status.c_str());
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!m_file_status.empty()) {
ImGui::Separator();
if (m_file_status_is_error) {
ImGui::TextWrapped("Status: %s", m_file_status.c_str());
}
else {
ImGui::TextWrapped("Status: %s", m_file_status.c_str());
}
if (!m_file_status.empty()) {
ImGui::Separator();
ImGui::TextWrapped("Status: %s", m_file_status.c_str());
}
🧰 Tools
🪛 Clang (14.0.6)

[note] 46-46: +3, including nesting penalty of 2, nesting level increased to 3

(clang)


[note] 48-48: +4, including nesting penalty of 3, nesting level increased to 4

(clang)


[note] 51-51: +1, nesting level increased to 4

(clang)


[warning] 48-48: if with identical then and else branches

(bugprone-branch-clone)


[note] 51-51: else branch starts here

(clang)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d947c799-6e83-40fb-82c2-510eab757035

📥 Commits

Reviewing files that changed from the base of the PR and between fb35057 and 526a17b.

📒 Files selected for processing (1)
  • .github/workflows/build.yml

Comment thread .github/workflows/build.yml Outdated
Comment thread .github/workflows/build.yml Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
vcpkg.json (1)

4-4: ⚠️ Potential issue | 🔴 Critical

Pin builtin-baseline to a real vcpkg commit SHA.

Line 4 is currently a build-breaker: "latest" is not a valid baseline value for manifest mode, and your pipeline failure confirms it. Treat this like pinning asset versions in an engine build—always deterministic.

Suggested fix
-  "builtin-baseline": "latest",
+  "builtin-baseline": "<40-char-vcpkg-commit-sha>",
#!/bin/bash
set -euo pipefail

# Verify builtin-baseline is pinned to a 40-char lowercase hex SHA.
python - <<'PY'
import json, re, sys
with open("vcpkg.json", "r", encoding="utf-8") as f:
    data = json.load(f)
baseline = data.get("builtin-baseline", "")
print("builtin-baseline:", baseline)
ok = bool(re.fullmatch(r"[0-9a-f]{40}", baseline))
print("valid_sha:", ok)
sys.exit(0 if ok else 1)
PY

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 143693af-8530-4e72-aeec-8d4ecaf48d57

📥 Commits

Reviewing files that changed from the base of the PR and between 526a17b and 9669c57.

📒 Files selected for processing (2)
  • .github/workflows/build.yml
  • vcpkg.json

Comment thread .github/workflows/build.yml Outdated
Comment on lines +23 to +24
configurePreset: 'default'
buildPreset: 'default'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate CMakePresets.json (case-insensitive search).
fd -i '^CMakePresets\.json$'

# Validate presence of required preset names.
python - <<'PY'
import json, pathlib, sys
p = pathlib.Path("CMakePresets.json")
if not p.exists():
    print("Missing CMakePresets.json at repository root")
    sys.exit(1)

data = json.loads(p.read_text(encoding="utf-8"))
cfg = {x.get("name") for x in data.get("configurePresets", [])}
bld = {x.get("name") for x in data.get("buildPresets", [])}
print("configurePresets:", sorted(cfg))
print("buildPresets:", sorted(bld))

missing = []
if "default" not in cfg:
    missing.append("configurePresets.default")
if "default" not in bld:
    missing.append("buildPresets.default")

if missing:
    print("Missing required presets:", ", ".join(missing))
    sys.exit(1)
PY

Repository: deanblackborough/Prune

Length of output: 110


Create CMakePresets.json with the default presets before merging.

The workflow references configurePresets.default and buildPresets.default, but the CMakePresets.json file doesn't exist at the repository root. Without it, your build pipeline will fail on the next run. You'll need to create the file and define both presets there—think of it like setting up your engine's build configurations before you can actually build anything.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 39d6c51b-07ad-4e05-98a5-e2e662630ee6

📥 Commits

Reviewing files that changed from the base of the PR and between 9669c57 and d7d9c29.

📒 Files selected for processing (2)
  • .github/workflows/build.yml
  • vcpkg.json

Comment thread .github/workflows/build.yml
coderabbitai[bot]
coderabbitai Bot previously requested changes Apr 19, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
.github/workflows/build.yml (1)

17-18: ⚠️ Potential issue | 🟠 Major

Pin lukka/run-vcpkg to an immutable commit SHA.

Good move adding vcpkg, but Line 18 still uses a floating ref (@v11). For deterministic and safer CI, pin to a full commit SHA.

Suggested change
-      - name: Set up vcpkg
-        uses: lukka/run-vcpkg@v11
+      - name: Set up vcpkg
+        uses: lukka/run-vcpkg@<full-commit-sha>
What is the full commit SHA for the latest stable lukka/run-vcpkg v11.x release tag?

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94a3e545-e968-4602-b48a-69b0b10044ba

📥 Commits

Reviewing files that changed from the base of the PR and between d7d9c29 and 30e7e01.

📒 Files selected for processing (2)
  • .github/workflows/build.yml
  • vcpkg.json

path: |
build/Release/Prune.exe
build/Prune.exe
path: build/Release/Prune.exe
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether workflow/cmake config indicates static linking or explicit DLL staging.
rg -n "VCPKG_TARGET_TRIPLET|x64-windows-static|CMAKE_TOOLCHAIN_FILE|upload-artifact|\\.dll|POST_BUILD|install\\(" .github/workflows/build.yml CMakeLists.txt

Repository: deanblackborough/Prune

Length of output: 314


🏁 Script executed:

# Get the full build workflow around the artifact upload section
cat -n .github/workflows/build.yml | sed -n '20,40p'

Repository: deanblackborough/Prune

Length of output: 667


🏁 Script executed:

# Check for vcpkg manifest and triplet configuration
fd -e json -e txt | grep -E "vcpkg|triplet" | head -20

Repository: deanblackborough/Prune

Length of output: 76


🏁 Script executed:

# Examine CMakeLists.txt for dependency handling and installation
cat -n CMakeLists.txt | head -60

Repository: deanblackborough/Prune

Length of output: 2127


🏁 Script executed:

# Check if there's any post-build script or explicit DLL copy logic
rg -n "add_custom_command|file\(COPY|install\(|dll|DLL" CMakeLists.txt -A 2

Repository: deanblackborough/Prune

Length of output: 48


🏁 Script executed:

# Check vcpkg.json for triplet specification and dependency details
cat vcpkg.json

Repository: deanblackborough/Prune

Length of output: 161


Runtime DLLs missing from artifact upload.

When vcpkg builds SDL2 and yaml-cpp without an explicit static triplet like x64-windows-static, they compile as dynamic libraries. Your current workflow uploads only Prune.exe, which means anyone downloading the artifact on a clean Windows machine will hit a runtime crash looking for SDL2.dll and yaml-cpp.dll.

Since your vcpkg.json doesn't specify a triplet and your CMake configuration doesn't override it, the Windows runner defaults to dynamic linking. You need to bundle those runtime DLLs with the executable. Either adjust your artifact path to include them:

Suggested fix
       - name: Upload artifact
         uses: actions/upload-artifact@v4
         with:
           name: prune-windows
-          path: build/Release/Prune.exe
+          path: |
+            build/Release/Prune.exe
+            build/Release/*.dll
           retention-days: 30

Alternatively, lock the vcpkg triplet to x64-windows-static for fully self-contained binaries.

Comment thread vcpkg.json Outdated
@deanblackborough deanblackborough dismissed coderabbitai[bot]’s stale review April 19, 2026 01:16

This is all initial save and load and it is known to be bad, it wll however do for now

@deanblackborough deanblackborough merged commit 4d03c29 into main Apr 19, 2026
2 checks passed
@deanblackborough deanblackborough deleted the save-and-load-to-yml branch April 19, 2026 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant