Skip to content

Add resource root entity#190

Open
CrosRoad95 wants to merge 2 commits intoMafiaHub:developfrom
CrosRoad95:feature/resource-root-entity
Open

Add resource root entity#190
CrosRoad95 wants to merge 2 commits intoMafiaHub:developfrom
CrosRoad95:feature/resource-root-entity

Conversation

@CrosRoad95
Copy link
Copy Markdown
Contributor

@CrosRoad95 CrosRoad95 commented Mar 28, 2026

This pull request adds to each resource a root entity which can be used as an handle for all entities created for given resource. When entity get added as a child of resource its lifetime is tied to resource lifetime and get automatically removed when resource stopped.

In flecs explorer an example created entity will be visible in hierarchy:
image

Summary by CodeRabbit

  • New Features

    • Resources are integrated with the world engine, have a root entity, and now automatically remove child entities during stop/error transitions.
  • Refactor

    • Resource ownership and manager hierarchy moved to the world/entity system so resource entities are parented under a manager root.
  • Chores

    • Public constructors updated to accept a world context; tests updated accordingly.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 28, 2026

Walkthrough

Integrates Flecs ECS into Resource and ResourceManager: Resources gain a root Flecs entity and OwnedResource component; ResourceManager creates a shared "Resources" root and parents resource roots to it; child entities are deferred-destroyed when resources stop or on teardown.

Changes

Cohort / File(s) Summary
Resource header & impl
code/framework/src/scripting/resource/resource.h, code/framework/src/scripting/resource/resource.cpp
Constructor now takes flecs::world*; adds _rootEntity, GetRootEntity(), OwnedResource struct, move semantics adjusted to transfer entity, destructor and DestroyChildEntities() added, and TransitionTo() triggers child cleanup on stop/error.
ResourceManager header & impl
code/framework/src/scripting/resource/resource_manager.h, code/framework/src/scripting/resource/resource_manager.cpp
Constructor now accepts flecs::world* and stores _world and a _rootEntity (entity("Resources")); discovered resources are constructed with the world and parented under manager _rootEntity; manager destructor destructs _rootEntity.
Integrations: server & client modules
code/framework/src/integrations/client/scripting/module.cpp, code/framework/src/integrations/server/scripting/module.cpp
Init updated to pass _world->GetWorld() into ResourceManager construction.
Tests updated
code/tests/modules/*.h (e.g., js_features_ut.h, resource_manager_ut.h, resource_ut.h, timer_context_ut.h)
Tests instantiate local flecs::world and pass &world to Resource/ResourceManager; new tests assert root entity creation, OwnedResource linkage, and child-entity removal on StopResource.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ResMgr as ResourceManager
    participant World as FlecsWorld
    participant Resource
    participant Flecs as FlecsRuntime

    Client->>ResMgr: Init(jsEngine, world, config)
    ResMgr->>World: entity("Resources")
    World-->>ResMgr: _rootEntity

    Client->>ResMgr: DiscoverResource(manifest)
    ResMgr->>Resource: new Resource(path, world)
    Resource->>World: entity(manifest_name)
    World-->>Resource: _rootEntity
    Resource->>Flecs: add OwnedResource(component -> Resource*)
    Resource-->>ResMgr: GetRootEntity()
    ResMgr->>Flecs: child_of(resource._rootEntity, ResMgr._rootEntity)

    Client->>Resource: TransitionTo(Stopped)
    Resource->>Resource: DestroyChildEntities()
    Resource->>Flecs: defer destroy(child entities)
    Flecs->>Flecs: execute deferred destruction

    Client->>ResMgr: Destroy ResourceManager
    ResMgr->>Flecs: destruct ResMgr._rootEntity
    Flecs->>Flecs: cascade destroy resource root entities
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • zpl-zak

Poem

🐇 In burrows of code where entities play,

I stitch roots to roots and chase crumbs away,
Children fall gently when Stop whistles near,
Owned and tended — then buried with cheer,
Hooray for tidy warrens, squeak-squeak, hip hooray!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% 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 pull request title 'Add resource root entity' directly and clearly describes the main change: introducing a root entity for each resource in the Flecs ECS system, which is the primary purpose of the changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/framework/src/scripting/resource/resource.cpp (1)

44-57: ⚠️ Potential issue | 🟠 Major

Transfer _rootEntity ownership when moving Resource.

Lines 53 and 71-76 copy the Flecs handle instead of transferring it. That leaves OwnedResource pointing at the moved-from object, the move ctor keeps other._rootEntity live so the moved-from destructor can delete the same entity, and move-assignment overwrites any existing root without cleaning it up. Either delete the move operations or fully transfer/rebind the handle.

Also applies to: 59-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/framework/src/scripting/resource/resource.cpp` around lines 44 - 57, The
move constructor and move-assignment for Resource currently copy the Flecs
entity handle (_rootEntity) leaving the moved-from object owning the same
entity; update Resource::Resource(Resource&&) to transfer ownership by
rebind/steal the Flecs handle into this->_rootEntity and then reset
other._rootEntity to an empty/null/invalid state so the moved-from destructor
won't destroy it, and update Resource::operator=(Resource&&) to first clean
up/destroy any existing _rootEntity owned by *this (matching OwnedResource
destructor semantics) before stealing the source's handle and nulling
other._rootEntity; alternatively, if transfer is not desired, delete the move
ctor/assignment to avoid double-destroy—make changes around _rootEntity,
Resource::Resource(Resource&&), Resource::operator=(Resource&&) and
OwnedResource cleanup to ensure exclusive ownership is maintained.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/framework/src/scripting/resource/resource_manager.cpp`:
- Around line 44-46: Check for null before dereferencing
CoreModules::GetWorldEngine() and its GetWorld() result when initializing
_rootEntity in ResourceManager; if either is null, avoid calling
entity("Resources") (set _rootEntity to a safe null/invalid state) and defer
root creation until the world is available (e.g., add a lazy init or a BuildRoot
method invoked after module registration). Update the constructor/site that
calls _rootEntity =
CoreModules::GetWorldEngine()->GetWorld()->entity("Resources") to guard using
CoreModules::GetWorldEngine() and CoreModules::GetWorldEngine()->GetWorld() and
ensure ResourceManager later attempts to create/attach the "Resources" root when
those getters return valid pointers.

In `@code/framework/src/scripting/resource/resource.cpp`:
- Around line 299-301: The code only calls DestroyChildEntities() when newState
== ResourceState::Stopped, which leaves child entities alive on error/restart
paths; update state-transition handling in the method that sets resource state
(the block around ResourceState changes) so that DestroyChildEntities() is also
invoked when transitioning to ResourceState::Error or during restart/failure
flows (ensure it runs when StartResource()/SetError() or
HandleResourceRuntimeError()/RestartResource() cause a non-running state), and
ensure StopResource()/IsRunning() logic does not early-return before child
cleanup; locate and modify the state-change logic and calls to
DestroyChildEntities(), StartResource, SetError, HandleResourceRuntimeError,
RestartResource, StopResource, and IsRunning to guarantee children are destroyed
on error and before restart.
- Around line 32-33: Validate the ECS world pointers before creating the root
entity: check that CoreModules::GetWorldEngine() returns non-null and that
GetWorld() returns non-null before calling GetWorld()->entity(...). If either is
null, avoid dereferencing, do not set _rootEntity or OwnedResource, and instead
handle the failure cleanly (e.g., set _rootEntity to an empty/invalid entity,
return/propagate an error state, and/or log a descriptive message referencing
_manifest.GetName()). Update the code around _rootEntity,
CoreModules::GetWorldEngine(), GetWorld(), _manifest, and OwnedResource so null
pointers are guarded and resource discovery cannot crash during early
initialization.

---

Outside diff comments:
In `@code/framework/src/scripting/resource/resource.cpp`:
- Around line 44-57: The move constructor and move-assignment for Resource
currently copy the Flecs entity handle (_rootEntity) leaving the moved-from
object owning the same entity; update Resource::Resource(Resource&&) to transfer
ownership by rebind/steal the Flecs handle into this->_rootEntity and then reset
other._rootEntity to an empty/null/invalid state so the moved-from destructor
won't destroy it, and update Resource::operator=(Resource&&) to first clean
up/destroy any existing _rootEntity owned by *this (matching OwnedResource
destructor semantics) before stealing the source's handle and nulling
other._rootEntity; alternatively, if transfer is not desired, delete the move
ctor/assignment to avoid double-destroy—make changes around _rootEntity,
Resource::Resource(Resource&&), Resource::operator=(Resource&&) and
OwnedResource cleanup to ensure exclusive ownership is maintained.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2dce8884-857b-4bee-acaf-b575549a52bd

📥 Commits

Reviewing files that changed from the base of the PR and between 1429bef and 915140d.

📒 Files selected for processing (4)
  • code/framework/src/scripting/resource/resource.cpp
  • code/framework/src/scripting/resource/resource.h
  • code/framework/src/scripting/resource/resource_manager.cpp
  • code/framework/src/scripting/resource/resource_manager.h

Comment on lines +44 to 46

_rootEntity = CoreModules::GetWorldEngine()->GetWorld()->entity("Resources");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validate the ECS world before building the manager root.

Line 45 dereferences CoreModules::GetWorldEngine() and GetWorld() without a guard. Those getters are raw pointers, so constructing ResourceManager before module registration turns this into a null dereference, and later discovery will try to parent resources into a missing root.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/framework/src/scripting/resource/resource_manager.cpp` around lines 44 -
46, Check for null before dereferencing CoreModules::GetWorldEngine() and its
GetWorld() result when initializing _rootEntity in ResourceManager; if either is
null, avoid calling entity("Resources") (set _rootEntity to a safe null/invalid
state) and defer root creation until the world is available (e.g., add a lazy
init or a BuildRoot method invoked after module registration). Update the
constructor/site that calls _rootEntity =
CoreModules::GetWorldEngine()->GetWorld()->entity("Resources") to guard using
CoreModules::GetWorldEngine() and CoreModules::GetWorldEngine()->GetWorld() and
ensure ResourceManager later attempts to create/attach the "Resources" root when
those getters return valid pointers.

Copy link
Copy Markdown

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
code/framework/src/scripting/resource/resource.cpp (1)

43-76: ⚠️ Potential issue | 🔴 Critical

Fix root-entity ownership transfer in the move operations.

_rootEntity is copied here, but the entity still points at other via OwnedResource, the move ctor leaves other._rootEntity live, and move assignment overwrites an existing root without destroying it first. That lets a moved-from Resource tear down the moved-to root and leaks the old target root.

Possible fix
     Resource::Resource(Resource &&other) noexcept
         : _path(std::move(other._path))
         , _manifest(std::move(other._manifest))
         , _manifestValid(other._manifestValid)
         , _state(other._state)
         , _errorMessage(std::move(other._errorMessage))
         , _stateTimestamp(other._stateTimestamp)
         , _loadTimestamp(other._loadTimestamp)
         , _isolate(other._isolate)
         , _rootEntity(other._rootEntity)
         , _exports(std::move(other._exports))
         , _restartAttempts(std::move(other._restartAttempts)) {
+        if (_rootEntity.is_valid()) {
+            _rootEntity.set<OwnedResource>({this});
+        }
         other._isolate = nullptr;
+        other._rootEntity = {};
     }

     Resource &Resource::operator=(Resource &&other) noexcept {
         if (this != &other) {
+            if (_rootEntity.is_valid()) {
+                _rootEntity.destruct();
+            }
             ClearExports();

             _path = std::move(other._path);
             _manifest = std::move(other._manifest);
             _manifestValid = other._manifestValid;
             _state = other._state;
             _errorMessage = std::move(other._errorMessage);
             _stateTimestamp = other._stateTimestamp;
             _loadTimestamp = other._loadTimestamp;
             _isolate = other._isolate;
             _rootEntity = other._rootEntity;
             _exports = std::move(other._exports);
             _restartAttempts = std::move(other._restartAttempts);

+            if (_rootEntity.is_valid()) {
+                _rootEntity.set<OwnedResource>({this});
+            }
             other._isolate = nullptr;
             other._rootEntity = {};
         }
         return *this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/framework/src/scripting/resource/resource.cpp` around lines 43 - 76, The
move ctor and move-assignment are copying _rootEntity instead of transferring
ownership, leaving other._rootEntity live and risking double-destroy/leak;
change the move ctor (Resource::Resource(Resource&&)) to move the root (use
_rootEntity(std::move(other._rootEntity)) in the initializer list) and then
clear other._rootEntity (set to {}), and in Resource::operator=(Resource&&)
ensure you destroy/reset the existing root before overwriting (call the existing
root teardown helper or reset _rootEntity) and then assign _rootEntity =
std::move(other._rootEntity) and clear other._rootEntity = {}; ensure ownership
semantics align with OwnedResource so the moved-from Resource no longer
references the entity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/framework/src/scripting/resource/resource.cpp`:
- Around line 298-300: The code currently calls DestroyChildEntities() whenever
newState == ResourceState::Stopped || newState == ResourceState::Error, which
causes child ECS entities to be destroyed even for non-terminal Error
transitions (SetError() is invoked earlier and errorBehavior can be "continue").
Update the condition in the state-change handling (the block using newState and
calling DestroyChildEntities()) so that child cleanup only happens for terminal
stop/restart paths (e.g., ResourceState::Stopped and any explicit terminal
restart state) and not for ResourceState::Error; keep SetError() and
errorBehavior logic intact so error transitions that should continue do not
trigger DestroyChildEntities(). Ensure you reference DestroyChildEntities(),
ResourceState::Stopped, ResourceState::Error and SetError() when making the
change.

---

Outside diff comments:
In `@code/framework/src/scripting/resource/resource.cpp`:
- Around line 43-76: The move ctor and move-assignment are copying _rootEntity
instead of transferring ownership, leaving other._rootEntity live and risking
double-destroy/leak; change the move ctor (Resource::Resource(Resource&&)) to
move the root (use _rootEntity(std::move(other._rootEntity)) in the initializer
list) and then clear other._rootEntity (set to {}), and in
Resource::operator=(Resource&&) ensure you destroy/reset the existing root
before overwriting (call the existing root teardown helper or reset _rootEntity)
and then assign _rootEntity = std::move(other._rootEntity) and clear
other._rootEntity = {}; ensure ownership semantics align with OwnedResource so
the moved-from Resource no longer references the entity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 562c1d79-7cc3-4958-9955-9f367610becf

📥 Commits

Reviewing files that changed from the base of the PR and between 915140d and 93da911.

📒 Files selected for processing (10)
  • code/framework/src/integrations/client/scripting/module.cpp
  • code/framework/src/integrations/server/scripting/module.cpp
  • code/framework/src/scripting/resource/resource.cpp
  • code/framework/src/scripting/resource/resource.h
  • code/framework/src/scripting/resource/resource_manager.cpp
  • code/framework/src/scripting/resource/resource_manager.h
  • code/tests/modules/js_features_ut.h
  • code/tests/modules/resource_manager_ut.h
  • code/tests/modules/resource_ut.h
  • code/tests/modules/timer_context_ut.h
🚧 Files skipped from review as they are similar to previous changes (2)
  • code/framework/src/scripting/resource/resource_manager.h
  • code/framework/src/scripting/resource/resource_manager.cpp

Comment on lines +298 to +300
if (newState == ResourceState::Stopped || newState == ResourceState::Error) {
DestroyChildEntities();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't destroy children on every Error transition.

Lines 720-732 of code/framework/src/scripting/resource/resource_manager.cpp call SetError() before applying errorBehavior. With errorBehavior == "continue", this branch now wipes the resource's ECS subtree even though the resource is intentionally left running. Child cleanup needs to stay on terminal stop/restart paths instead of being tied directly to ResourceState::Error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/framework/src/scripting/resource/resource.cpp` around lines 298 - 300,
The code currently calls DestroyChildEntities() whenever newState ==
ResourceState::Stopped || newState == ResourceState::Error, which causes child
ECS entities to be destroyed even for non-terminal Error transitions (SetError()
is invoked earlier and errorBehavior can be "continue"). Update the condition in
the state-change handling (the block using newState and calling
DestroyChildEntities()) so that child cleanup only happens for terminal
stop/restart paths (e.g., ResourceState::Stopped and any explicit terminal
restart state) and not for ResourceState::Error; keep SetError() and
errorBehavior logic intact so error transitions that should continue do not
trigger DestroyChildEntities(). Ensure you reference DestroyChildEntities(),
ResourceState::Stopped, ResourceState::Error and SetError() when making the
change.

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.

1 participant