Skip to content

feat(physics): internalize jolt bodyid entity mapping inside the physics plugin#500

Merged
Miou-zora merged 13 commits into
mainfrom
496-feature-internalize-jolt-bodyid-entity-mapping-inside-the-physics-plugin
Mar 19, 2026
Merged

feat(physics): internalize jolt bodyid entity mapping inside the physics plugin#500
Miou-zora merged 13 commits into
mainfrom
496-feature-internalize-jolt-bodyid-entity-mapping-inside-the-physics-plugin

Conversation

@Miou-zora

@Miou-zora Miou-zora commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

Previously, every project using the physics plugin had to manually maintain a PhysicsBodyToEntityMap resource — a bidirectional map between Jolt BodyIDs and engine Entity handles. This was error-prone boilerplate that didn't belong in user code.

This PR internalizes the BodyIDEntity mapping directly inside the Physics plugin. A BodyEntityMap resource (backed by a new generic BiMap<TLeft, TRight> utility) is now automatically registered by PhysicsPlugin and kept in sync by the RigidBodySystem and SoftBodySystem on body creation and destruction. Projects using physics no longer need to declare or synchronize this map themselves.

Related Issues

Fixes #496

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Code refactoring

Changes Made

  • Added Utils::BiMap<TLeft, TRight>: a generic bidirectional map backed by two unordered_maps, supporting Add, Remove (by either key), Get, and Contains.
  • Added Resource::BodyEntityMap as a type alias for BiMap<Engine::EntityId, JPH::BodyID>.
  • Registered BodyEntityMap as a built-in resource in PhysicsPlugin::Bind().
  • Updated RigidBodySystem and SoftBodySystem to automatically insert/remove entries in BodyEntityMap on body construct/destroy callbacks.
  • Added std::hash<Engine::EntityId> specialization required for use in unordered_map.
  • Added Entity::Kill() method and updated internal systems to use the higher-level Engine::Entity API instead of raw registry calls.
  • Added unit tests: BodyEntityMapTest and BodyEntitySyncTest.

Testing

  • Unit tests pass (xmake test)
  • Code follows the project's style guidelines (clang-format)

Test Environment

  • OS: macOS
  • Compiler: Clang

Screenshots/Videos (if applicable)

None

Documentation

  • No documentation have been made for the moment

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Breaking Changes

None. The BodyEntityMap resource is now registered automatically by the plugin — projects that previously registered their own PhysicsBodyToEntityMap should remove it to avoid duplication, but it is not required immediately.

Additional Notes

This was identified via a // TODO audit of the ES-Factoria demo project. Validation step: remove PhysicsBodyToEntityMap from ES-Factoria and confirm everything still works via BodyEntityMap.

Summary by CodeRabbit

  • New Features

    • Added explicit entity termination API
    • Added bidirectional entity↔physics-body mapping and entity-id hashing
  • Refactor

    • Physics systems moved to entity-centric APIs with clearer lifecycle hooks
    • Physics plugin now registers the new mapping resource and shutdown handling
  • Tests

    • New unit and integration tests verifying mapping behavior, lifecycle sync, and physics test flows
  • Documentation

    • Added commit-message style guidelines

@Miou-zora Miou-zora self-assigned this Mar 18, 2026
@Miou-zora Miou-zora added the enhancement New feature or request label Mar 18, 2026
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

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

Adds Entity::Kill and EntityId hashing; introduces a BiMap utility and BodyEntityMap resource; wires BodyID↔Entity synchronization into rigid/soft body systems with entt hooks and plugin registration; updates physics tests and adds commit-message guidance.

Changes

Cohort / File(s) Summary
Entity API
src/engine/src/entity/Entity.hpp, src/engine/src/entity/Entity.cpp, src/engine/src/entity/EntityId.hpp
Add Entity::Kill() which calls Core::KillEntity; add std::hash<Engine::EntityId> specialization to allow hashing EntityId.
Physics Plugin Core
src/plugin/physics/src/Physics.hpp, src/plugin/physics/src/plugin/PhysicsPlugin.cpp
Include BiMap/BodyEntityMap headers; register Resource::BodyEntityMap with the plugin and adjust one resource registration call.
BiMap Utility
src/plugin/physics/src/utils/BiMap.hpp
New header-only Physics::Utils::BiMap<TLeft,TRight> providing bidirectional one-to-one mapping with Add/Remove/Get/Contains/Size; move-only semantics.
BodyEntityMap Resource
src/plugin/physics/src/resource/BodyEntityMap.hpp
New alias Physics::Resource::BodyEntityMap = Utils::BiMap<Engine::EntityId, JPH::BodyID> for plugin-wide bidirectional mapping.
Rigid Body System
src/plugin/physics/src/system/RigidBodySystem.hpp, src/plugin/physics/src/system/RigidBodySystem.cpp
Refactor to use Engine::Entity APIs for component access; set JPH user-data to entity.Id(); add OnRigidBodyInternalConstruct/OnRigidBodyInternalDestroy hooks; add ShutdownRigidBodySystem to disconnect hooks; update BodyEntityMap on construct/destroy.
Soft Body System
src/plugin/physics/src/system/SoftBodySystem.cpp
Attach internal component via entity.AddComponent; add OnSoftBodyInternalConstruct/OnSoftBodyInternalDestroy hooks; register/unregister hooks and update BodyEntityMap accordingly.
Tests — BodyEntityMap
src/plugin/physics/tests/BodyEntityMapTest.cpp, src/plugin/physics/tests/BodyEntitySyncTest.cpp
New unit/integration tests verifying Add/Remove/Get/Contains, and lifecycle synchronization between entities and BodyEntityMap.
Tests — Scheduler refactor
src/plugin/physics/tests/PhysicsTest.cpp, .../VehicleForwardMovementTest.cpp, .../VehicleGravityDropTest.cpp, .../VehicleWheelTransformSyncTest.cpp
Refactor multiple physics tests to use scheduler-driven Startup/Update/Shutdown systems and local TestScheduler classes instead of manual stepping loops.
Docs / Policies
tools/ai/copilot/copilot-commit-message-instructions.md
Add commit message convention guidance for copilot tooling.

Sequence Diagram(s)

sequenceDiagram
    actor Dev as Developer
    participant Engine as Engine (Core)
    participant Plugin as PhysicsPlugin
    participant System as RigidBodySystem
    participant Map as BodyEntityMap
    participant JPH as Jolt Physics

    Dev->>Engine: create Entity + add RigidBody/SoftBody
    Engine->>System: notify construct handlers
    System->>JPH: create physics body
    JPH-->>System: return BodyID
    System->>Map: Add(EntityId, BodyID)
    Map-->>System: acknowledge stored

    Dev->>Engine: destroy Entity (or call Entity::Kill)
    Engine->>System: notify destroy handlers
    System->>Map: Remove(EntityId / BodyID)
    Map-->>System: acknowledge removed
    System->>JPH: destroy physics body
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ripel2
  • Divengerss

Poem

🐰 I stitched a BiMap, left to right and back,
I gave each body an ID and kept no track slack.
A Kill hop here, a hash hop there,
Entities and bodies now pair with care.
Hooray — the physics plugin found its knack.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the main feature: internalizing Jolt BodyID entity mapping into the physics plugin, which is the primary objective of this PR.
Linked Issues check ✅ Passed Changes fully implement issue #496 requirements: BiMap utility added [BiMap.hpp], BodyEntityMap resource registered in plugin [PhysicsPlugin.cpp], automatic entity-body mapping maintained on creation/destruction [RigidBodySystem.cpp, SoftBodySystem.cpp], hash specialization added for EntityId [EntityId.hpp], and comprehensive tests validate the mapping lifecycle [BodyEntityMapTest.cpp, BodyEntitySyncTest.cpp].
Out of Scope Changes check ✅ Passed All changes directly support internalizing the BodyID-Entity mapping. Supporting changes include Entity::Kill() for lifecycle management [Entity.hpp/cpp], ShutdownRigidBodySystem for proper cleanup [RigidBodySystem.hpp/cpp], test refactoring to use scheduler-driven flow [PhysicsTest.cpp and vehicle tests], and documentation conventions [copilot-commit-message-instructions.md].

✏️ 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 496-feature-internalize-jolt-bodyid-entity-mapping-inside-the-physics-plugin
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (2)
src/plugin/physics/src/system/SoftBodySystem.cpp (1)

546-555: ⚠️ Potential issue | 🟡 Minor

Missing disconnect for OnSoftBodyInternalDestroy in shutdown.

InitSoftBodySystem connects OnSoftBodyInternalDestroy (line 541), but ShutdownSoftBodySystem does not disconnect it. This could cause issues if the system is reinitialized or if callbacks fire after shutdown.

🔧 Suggested fix
 void ShutdownSoftBodySystem(Engine::Core &core)
 {
     auto &registry = core.GetRegistry();
 
     // Disconnect hooks
     registry.on_construct<Component::SoftBody>().disconnect<&OnSoftBodyConstruct>();
     registry.on_destroy<Component::SoftBody>().disconnect<&OnSoftBodyDestroy>();
+    registry.on_destroy<Component::SoftBodyInternal>().disconnect<&OnSoftBodyInternalDestroy>();
 
     Log::Debug("SoftBodySystem shutdown");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 546 - 555,
ShutdownSoftBodySystem is missing a disconnect for the handler registered in
InitSoftBodySystem: add a disconnect call for OnSoftBodyInternalDestroy in
ShutdownSoftBodySystem (mirror the existing disconnects for
OnSoftBodyConstruct/OnSoftBodyDestroy) so the registry no longer holds the
callback (i.e., call
registry.on_destroy<Component::SoftBodyInternal>().disconnect<&OnSoftBodyInternalDestroy>();
or the exact event type used when connecting) to prevent callbacks after
shutdown.
src/plugin/physics/src/system/RigidBodySystem.cpp (1)

411-420: ⚠️ Potential issue | 🟡 Minor

OnRigidBodyInternalConstruct is dead code and should be removed.

OnRigidBodyInternalConstruct (lines 331-345) is implemented to add mappings to BodyEntityMap, but it is never registered in InitRigidBodySystem. Only the destroy hook is connected (line 419).

The function cannot be triggered because RigidBodyInternal components are only ever created internally via OnRigidBodyConstruct (line 319), and the mapping to BodyEntityMap already occurs there. No external construction of RigidBodyInternal exists in the codebase.

Either remove the unused function or add a comment explaining why it exists.

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

In `@src/plugin/physics/src/system/RigidBodySystem.cpp` around lines 411 - 420,
Remove the dead function OnRigidBodyInternalConstruct (or add a comment) because
it is never registered in InitRigidBodySystem and its behavior (adding mappings
to BodyEntityMap) is already handled in OnRigidBodyConstruct; specifically
delete OnRigidBodyInternalConstruct (or, if you prefer to keep it, add a clear
comment on top of OnRigidBodyInternalConstruct explaining it is unused/kept for
historical reasons) and ensure InitRigidBodySystem only registers the actual
handlers (OnRigidBodyConstruct, OnRigidBodyDestroy, OnRigidBodyInternalDestroy)
so there is no misleading, unreferenced code.
🧹 Nitpick comments (7)
tools/ai/copilot/copilot-commit-message-instructions.md (1)

5-13: Consider adding a language specifier to the fenced code block.

The fenced code block lacks a language identifier. Adding text would satisfy the linter and improve rendering consistency across markdown viewers.

✨ Proposed improvement
-```
+```text
 <type>(<scope>): <short summary>
   │       │             │
   │       │             └─⫸ Summary in present tense. Not capitalized. No period at the end.
   │       │
   │       └─⫸ Commit Scope
   │
   └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test|chore
 ```

As per static analysis hints, markdownlint flags fenced code blocks without language specifiers.

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

In `@tools/ai/copilot/copilot-commit-message-instructions.md` around lines 5 - 13,
The fenced code block that contains the commit template starting with
"<type>(<scope>): <short summary>" lacks a language specifier; update the
opening fence from ``` to ```text so the block becomes "```text" (and keep the
closing fence as ```), which satisfies markdownlint and ensures consistent
rendering.
src/plugin/physics/src/utils/BiMap.hpp (1)

7-10: Consider explicitly deleting copy/move operations.

The class stores pointers to keys within its internal maps. The implicitly-generated copy constructor and copy assignment operator would copy these pointers, which would then point into the source object's maps—causing undefined behavior if the copy is used after the source is modified or destroyed.

Since BodyEntityMap is used as a singleton resource, this may not be an immediate problem, but explicitly deleting these operations would prevent accidental misuse.

♻️ Suggested change
 template <typename TLeft, typename TRight> class BiMap {
   public:
     BiMap() = default;
     ~BiMap() = default;
+    BiMap(const BiMap &) = delete;
+    BiMap &operator=(const BiMap &) = delete;
+    BiMap(BiMap &&) = delete;
+    BiMap &operator=(BiMap &&) = delete;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/src/utils/BiMap.hpp` around lines 7 - 10, The BiMap
template should explicitly disable copying/moving to avoid dangling internal
pointer issues; update the BiMap class declaration (and any singleton wrapper
like BodyEntityMap usage) to delete the copy constructor, copy assignment
operator, move constructor, and move assignment operator (e.g., declare
BiMap(const BiMap&) = delete; BiMap& operator=(const BiMap&) = delete;
BiMap(BiMap&&) = delete; BiMap& operator=(BiMap&&) = delete;) so instances
cannot be copied or moved accidentally and pointers into its internal maps
remain valid.
src/plugin/physics/tests/BodyEntityMapTest.cpp (1)

5-77: Good test coverage for basic operations.

The tests cover addition, removal, retrieval, and containment checks. Consider adding tests for edge cases:

  1. Updating an existing mapping — verifying that Add(existingLeft, newRight) properly replaces the mapping (this would catch the bug identified in BiMap::Add)
  2. Removing non-existent keys — ensuring Remove() is safe when the key doesn't exist
  3. Get() on missing key — documenting that it throws std::out_of_range
🧪 Example additional test case for update behavior
TEST(PhysicsPlugin, BodyEntityMapUpdate)
{
    Physics::Resource::BodyEntityMap map;

    map.Add(Engine::EntityId{1}, JPH::BodyID{10});
    EXPECT_EQ(map.Get(Engine::EntityId{1}), JPH::BodyID{10});

    // Update mapping for same entity to different body
    map.Add(Engine::EntityId{1}, JPH::BodyID{20});
    
    EXPECT_EQ(map.Size(), 1);  // Should still be 1, not 2
    EXPECT_EQ(map.Get(Engine::EntityId{1}), JPH::BodyID{20});
    EXPECT_FALSE(map.Contains(JPH::BodyID{10}));  // Old body should be removed
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/tests/BodyEntityMapTest.cpp` around lines 5 - 77, Tests
miss edge cases: update existing mapping, removing non-existent keys, and Get()
on missing keys; add tests and fix BiMap::Add so Add(existingLeft, newRight)
replaces the old right and updates both directions. Specifically, modify
BiMap::Add to check if left or right already exist and remove their old pairings
before inserting (ensure it updates both maps used by
Physics::Resource::BodyEntityMap's Add/Remove/Get/Contains), make Remove safe
when key absent (no-ops), and ensure Get throws std::out_of_range for missing
keys so tests can assert that behavior; then add the suggested TEST
PhysicsPlugin.BodyEntityMapUpdate plus cases for Remove non-existent and Get
missing-key.
src/plugin/physics/src/system/SoftBodySystem.cpp (2)

446-447: Same duplicate addition issue as RigidBodySystem.

OnSoftBodyConstruct adds to BodyEntityMap (line 447), and OnSoftBodyInternalConstruct (lines 461-476) would also add when triggered. However, OnSoftBodyInternalConstruct is not registered in InitSoftBodySystem, making it dead code.

For consistency with the destruction pattern, either:

  1. Remove the dead OnSoftBodyInternalConstruct function, or
  2. Register it and remove the Add call from OnSoftBodyConstruct

Also applies to: 461-476

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

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 446 - 447, The
code currently duplicates BodyEntityMap additions because OnSoftBodyConstruct
calls core.GetResource<Resource::BodyEntityMap>().Add(entity, bodyID) while
OnSoftBodyInternalConstruct (defined but not used) would also add the same
mapping; fix by choosing one approach: either remove the dead
OnSoftBodyInternalConstruct function entirely, or register it in
InitSoftBodySystem and remove the Add(...) call from OnSoftBodyConstruct so the
mapping is added only inside OnSoftBodyInternalConstruct; update
InitSoftBodySystem to subscribe the OnSoftBodyInternalConstruct handler if you
pick registration, or delete the OnSoftBodyInternalConstruct symbol and any
unused references if you pick removal to match the destruction pattern.

502-503: Duplicate removal from BodyEntityMap (same as RigidBodySystem).

OnSoftBodyDestroy removes from BodyEntityMap (line 502) and then removes SoftBodyInternal (line 503), which triggers OnSoftBodyInternalDestroy to remove again (line 525).

Apply the same fix as suggested for RigidBodySystem - let OnSoftBodyInternalDestroy handle the removal consistently.

♻️ Suggested consolidation
         Log::Debug(fmt::format("Destroyed SoftBody for entity {}", entity));
 
-        core.GetResource<Resource::BodyEntityMap>().Remove(internal->bodyID);
         entity.RemoveComponent<Component::SoftBodyInternal>();

Also applies to: 511-526

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

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 502 - 503,
OnSoftBodyDestroy currently removes the body from BodyEntityMap and then removes
the SoftBodyInternal component which causes OnSoftBodyInternalDestroy to remove
the same entry again; remove the explicit call to
core.GetResource<Resource::BodyEntityMap>().Remove(internal->bodyID) from
OnSoftBodyDestroy and only call
entity.RemoveComponent<Component::SoftBodyInternal>() so that
OnSoftBodyInternalDestroy is the single place that performs
core.GetResource<Resource::BodyEntityMap>().Remove(bodyID); apply the same
change for the duplicated block in the 511-526 region to consolidate removal
logic.
src/plugin/physics/src/system/RigidBodySystem.cpp (1)

382-383: Duplicate removal from BodyEntityMap.

OnRigidBodyDestroy removes the mapping from BodyEntityMap (line 382) and then removes the RigidBodyInternal component (line 383). Removing the component triggers OnRigidBodyInternalDestroy, which attempts to remove the same mapping again (line 404).

This mirrors the duplicate addition issue. Consider removing the BodyEntityMap.Remove() call from OnRigidBodyDestroy (line 382) and letting OnRigidBodyInternalDestroy handle it consistently.

♻️ Suggested consolidation
         Log::Debug(fmt::format("Destroyed RigidBody for entity {} with BodyID {}", entity,
                                internalComponent->bodyID.GetIndexAndSequenceNumber()));
 
-        core->GetResource<Resource::BodyEntityMap>().Remove(internalComponent->bodyID);
         entity.RemoveComponent<Component::RigidBodyInternal>();

This lets OnRigidBodyInternalDestroy handle the map removal consistently.

Also applies to: 391-405

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

In `@src/plugin/physics/src/system/RigidBodySystem.cpp` around lines 382 - 383,
OnRigidBodyDestroy currently calls
core->GetResource<Resource::BodyEntityMap>().Remove(internalComponent->bodyID)
and then entity.RemoveComponent<Component::RigidBodyInternal>(), which causes
OnRigidBodyInternalDestroy to attempt the same removal again; remove the
explicit BodyEntityMap.Remove(...) call from OnRigidBodyDestroy and let
OnRigidBodyInternalDestroy perform the single authoritative removal of the
bodyID mapping when handling the Component::RigidBodyInternal destruction
(ensure any other paths that remove the component rely on that single removal).
src/plugin/physics/tests/BodyEntitySyncTest.cpp (1)

46-86: Missing error policy setting may cause test instability.

BodyEntityMapRemoval test (lines 46-86) does not call SetErrorPolicyForAllSchedulers, while other tests do (lines 17, 92). If entity.Kill() or other operations throw/log errors, this inconsistency could lead to different test behaviors.

🔧 Suggested fix
 TEST(PhysicsPlugin, BodyEntityMapRemoval)
 {
     Engine::Core core;
 
+    core.SetErrorPolicyForAllSchedulers(Engine::Scheduler::SchedulerErrorPolicy::Nothing);
+
     core.AddPlugins<Physics::Plugin>();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/tests/BodyEntitySyncTest.cpp` around lines 46 - 86, The
test BodyEntityMapRemoval omits calling SetErrorPolicyForAllSchedulers on the
Engine::Core instance, causing inconsistent error-handling compared to other
tests; update the test to invoke core.SetErrorPolicyForAllSchedulers(...) (using
the same ErrorPolicy used in the other tests) during setup—before
core.RunSystems() (for example right after core.AddPlugins or inside the
core.RegisterSystem setup) so that entity.Kill() and other operations use the
same error policy and the test behaves consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugin/physics/src/resource/BodyEntityMap.hpp`:
- Around line 12-13: BodyEntityMap instantiation fails because JPH::BodyID lacks
a std::hash specialization required by Utils::BiMap; add a
std::hash<JPH::BodyID> specialization that converts the BodyID to a stable
integral value (e.g., call the BodyID accessor that returns its raw/index value
or cast to its underlying integral type) and returns
std::hash<std::underlying_type_t<...>> applied to that value; place the
specialization in namespace std so unordered_map can use it and then
BodyEntityMap (aliasing Utils::BiMap<Engine::EntityId, JPH::BodyID>) will
compile.

In `@src/plugin/physics/src/system/RigidBodySystem.cpp`:
- Around line 319-320: Remove the redundant call to BodyEntityMap::Add in
OnRigidBodyConstruct: when you call
entity.AddComponent<Component::RigidBodyInternal>(bodyID) the
OnRigidBodyInternalConstruct handler will perform the same
core->GetResource<Resource::BodyEntityMap>().Add(entity, bodyID) mapping, so
delete the explicit Add() line after AddComponent. Update OnRigidBodyConstruct
to only add the RigidBodyInternal component and rely on
OnRigidBodyInternalConstruct (and the existing BiMap::Add behavior) to insert
the mapping.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Around line 471-472: The warning in OnSoftBodyInternalConstruct incorrectly
says "skipping body removal" for SoftBodyInternal; update the Log::Warn call in
the OnSoftBodyInternalConstruct handler to state "skipping body addition to
BodyEntityMap" (or similar) so it accurately reflects the construct path and
mentions SoftBodyInternal and BodyEntityMap for clarity; keep the early return
behavior unchanged.

In `@src/plugin/physics/src/utils/BiMap.hpp`:
- Around line 12-19: Add currently uses insert_or_assign on _leftToRight and
_rightToLeft but never cleans up the previous opposite mapping, leaving stale
entries; modify Add(const TLeft &left, const TRight &right) to: check if left
already exists in _leftToRight and if so get the old right (via the stored
pointer) and erase that old right from _rightToLeft before inserting the new
mapping; similarly check if right already exists in _rightToLeft and if so erase
its old left from _leftToRight to avoid orphaned pointers; then proceed to
insert_or_assign the new left→right and right→left and update the stored pointer
fields so both maps remain consistent (refer to Add, _leftToRight, _rightToLeft,
insert_or_assign).

In `@tools/ai/copilot/copilot-commit-message-instructions.md`:
- Line 49: Update the guideline text that reads "* commit must be in english" to
capitalize the language name: change "english" to "English" (locate the exact
string "* commit must be in english" in copilot-commit-message-instructions.md
and replace it so the rule reads "* commit must be in English").

---

Outside diff comments:
In `@src/plugin/physics/src/system/RigidBodySystem.cpp`:
- Around line 411-420: Remove the dead function OnRigidBodyInternalConstruct (or
add a comment) because it is never registered in InitRigidBodySystem and its
behavior (adding mappings to BodyEntityMap) is already handled in
OnRigidBodyConstruct; specifically delete OnRigidBodyInternalConstruct (or, if
you prefer to keep it, add a clear comment on top of
OnRigidBodyInternalConstruct explaining it is unused/kept for historical
reasons) and ensure InitRigidBodySystem only registers the actual handlers
(OnRigidBodyConstruct, OnRigidBodyDestroy, OnRigidBodyInternalDestroy) so there
is no misleading, unreferenced code.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Around line 546-555: ShutdownSoftBodySystem is missing a disconnect for the
handler registered in InitSoftBodySystem: add a disconnect call for
OnSoftBodyInternalDestroy in ShutdownSoftBodySystem (mirror the existing
disconnects for OnSoftBodyConstruct/OnSoftBodyDestroy) so the registry no longer
holds the callback (i.e., call
registry.on_destroy<Component::SoftBodyInternal>().disconnect<&OnSoftBodyInternalDestroy>();
or the exact event type used when connecting) to prevent callbacks after
shutdown.

---

Nitpick comments:
In `@src/plugin/physics/src/system/RigidBodySystem.cpp`:
- Around line 382-383: OnRigidBodyDestroy currently calls
core->GetResource<Resource::BodyEntityMap>().Remove(internalComponent->bodyID)
and then entity.RemoveComponent<Component::RigidBodyInternal>(), which causes
OnRigidBodyInternalDestroy to attempt the same removal again; remove the
explicit BodyEntityMap.Remove(...) call from OnRigidBodyDestroy and let
OnRigidBodyInternalDestroy perform the single authoritative removal of the
bodyID mapping when handling the Component::RigidBodyInternal destruction
(ensure any other paths that remove the component rely on that single removal).

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Around line 446-447: The code currently duplicates BodyEntityMap additions
because OnSoftBodyConstruct calls
core.GetResource<Resource::BodyEntityMap>().Add(entity, bodyID) while
OnSoftBodyInternalConstruct (defined but not used) would also add the same
mapping; fix by choosing one approach: either remove the dead
OnSoftBodyInternalConstruct function entirely, or register it in
InitSoftBodySystem and remove the Add(...) call from OnSoftBodyConstruct so the
mapping is added only inside OnSoftBodyInternalConstruct; update
InitSoftBodySystem to subscribe the OnSoftBodyInternalConstruct handler if you
pick registration, or delete the OnSoftBodyInternalConstruct symbol and any
unused references if you pick removal to match the destruction pattern.
- Around line 502-503: OnSoftBodyDestroy currently removes the body from
BodyEntityMap and then removes the SoftBodyInternal component which causes
OnSoftBodyInternalDestroy to remove the same entry again; remove the explicit
call to core.GetResource<Resource::BodyEntityMap>().Remove(internal->bodyID)
from OnSoftBodyDestroy and only call
entity.RemoveComponent<Component::SoftBodyInternal>() so that
OnSoftBodyInternalDestroy is the single place that performs
core.GetResource<Resource::BodyEntityMap>().Remove(bodyID); apply the same
change for the duplicated block in the 511-526 region to consolidate removal
logic.

In `@src/plugin/physics/src/utils/BiMap.hpp`:
- Around line 7-10: The BiMap template should explicitly disable copying/moving
to avoid dangling internal pointer issues; update the BiMap class declaration
(and any singleton wrapper like BodyEntityMap usage) to delete the copy
constructor, copy assignment operator, move constructor, and move assignment
operator (e.g., declare BiMap(const BiMap&) = delete; BiMap& operator=(const
BiMap&) = delete; BiMap(BiMap&&) = delete; BiMap& operator=(BiMap&&) = delete;)
so instances cannot be copied or moved accidentally and pointers into its
internal maps remain valid.

In `@src/plugin/physics/tests/BodyEntityMapTest.cpp`:
- Around line 5-77: Tests miss edge cases: update existing mapping, removing
non-existent keys, and Get() on missing keys; add tests and fix BiMap::Add so
Add(existingLeft, newRight) replaces the old right and updates both directions.
Specifically, modify BiMap::Add to check if left or right already exist and
remove their old pairings before inserting (ensure it updates both maps used by
Physics::Resource::BodyEntityMap's Add/Remove/Get/Contains), make Remove safe
when key absent (no-ops), and ensure Get throws std::out_of_range for missing
keys so tests can assert that behavior; then add the suggested TEST
PhysicsPlugin.BodyEntityMapUpdate plus cases for Remove non-existent and Get
missing-key.

In `@src/plugin/physics/tests/BodyEntitySyncTest.cpp`:
- Around line 46-86: The test BodyEntityMapRemoval omits calling
SetErrorPolicyForAllSchedulers on the Engine::Core instance, causing
inconsistent error-handling compared to other tests; update the test to invoke
core.SetErrorPolicyForAllSchedulers(...) (using the same ErrorPolicy used in the
other tests) during setup—before core.RunSystems() (for example right after
core.AddPlugins or inside the core.RegisterSystem setup) so that entity.Kill()
and other operations use the same error policy and the test behaves
consistently.

In `@tools/ai/copilot/copilot-commit-message-instructions.md`:
- Around line 5-13: The fenced code block that contains the commit template
starting with "<type>(<scope>): <short summary>" lacks a language specifier;
update the opening fence from ``` to ```text so the block becomes "```text" (and
keep the closing fence as ```), which satisfies markdownlint and ensures
consistent rendering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ba56f620-c1d1-4b23-a020-39bcc948c7f9

📥 Commits

Reviewing files that changed from the base of the PR and between af58b97 and ae194be.

📒 Files selected for processing (12)
  • src/engine/src/entity/Entity.cpp
  • src/engine/src/entity/Entity.hpp
  • src/engine/src/entity/EntityId.hpp
  • src/plugin/physics/src/Physics.hpp
  • src/plugin/physics/src/plugin/PhysicsPlugin.cpp
  • src/plugin/physics/src/resource/BodyEntityMap.hpp
  • src/plugin/physics/src/system/RigidBodySystem.cpp
  • src/plugin/physics/src/system/SoftBodySystem.cpp
  • src/plugin/physics/src/utils/BiMap.hpp
  • src/plugin/physics/tests/BodyEntityMapTest.cpp
  • src/plugin/physics/tests/BodyEntitySyncTest.cpp
  • tools/ai/copilot/copilot-commit-message-instructions.md

Comment on lines +12 to +13
namespace Physics::Resource {
using BodyEntityMap = Utils::BiMap<Engine::EntityId, JPH::BodyID>;

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

🌐 Web query:

Does Jolt Physics JPH::BodyID have std::hash specialization?

💡 Result:

No, Jolt Physics JPH::BodyID does not have a std::hash specialization.

Citations:


JPH::BodyID lacks std::hash specialization and will cause compilation failure.

The BiMap template requires both key types to be hashable. While Engine::EntityId now has a std::hash specialization, JPH::BodyID does not provide one. This will cause compilation errors when attempting to instantiate the BodyEntityMap type alias, as the underlying std::unordered_map cannot use JPH::BodyID without a hash function. A custom std::hash specialization for JPH::BodyID must be implemented or an alternative container type should be used.

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

In `@src/plugin/physics/src/resource/BodyEntityMap.hpp` around lines 12 - 13,
BodyEntityMap instantiation fails because JPH::BodyID lacks a std::hash
specialization required by Utils::BiMap; add a std::hash<JPH::BodyID>
specialization that converts the BodyID to a stable integral value (e.g., call
the BodyID accessor that returns its raw/index value or cast to its underlying
integral type) and returns std::hash<std::underlying_type_t<...>> applied to
that value; place the specialization in namespace std so unordered_map can use
it and then BodyEntityMap (aliasing Utils::BiMap<Engine::EntityId, JPH::BodyID>)
will compile.

Comment on lines +319 to +320
entity.AddComponent<Component::RigidBodyInternal>(bodyID);
core->GetResource<Resource::BodyEntityMap>().Add(entity, bodyID);

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
# Check BiMap::Add implementation to see if it handles duplicate insertions
ast-grep --pattern $'void Add($_, $_) {
  $$$
}'

# Also search for Add method in BiMap
rg -n -A 10 'void Add' --glob '*BiMap*'

Repository: EngineSquared/EngineSquared

Length of output: 946


Remove redundant BodyEntityMap::Add() call from OnRigidBodyConstruct.

The Add() call at line 320 is redundant because it will be immediately followed by an identical Add() call in OnRigidBodyInternalConstruct (triggered when the RigidBodyInternal component is created at line 319). While BiMap::Add uses insert_or_assign and is idempotent (safe to call with duplicate keys), the sequential calls are unnecessary. Remove line 320 and rely on OnRigidBodyInternalConstruct to handle the mapping.

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

In `@src/plugin/physics/src/system/RigidBodySystem.cpp` around lines 319 - 320,
Remove the redundant call to BodyEntityMap::Add in OnRigidBodyConstruct: when
you call entity.AddComponent<Component::RigidBodyInternal>(bodyID) the
OnRigidBodyInternalConstruct handler will perform the same
core->GetResource<Resource::BodyEntityMap>().Add(entity, bodyID) mapping, so
delete the explicit Add() line after AddComponent. Update OnRigidBodyConstruct
to only add the RigidBodyInternal component and rely on
OnRigidBodyInternalConstruct (and the existing BiMap::Add behavior) to insert
the mapping.

Comment on lines +471 to +472
Log::Warn("SoftBodyInternal has invalid BodyID, skipping body removal from BodyEntityMap");
return;

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

Incorrect log message - says "removal" in construct handler.

The warning message says "skipping body removal" but this is OnSoftBodyInternalConstruct, so it should say "skipping body addition".

📝 Suggested fix
     if (!internalComponent.IsValid())
     {
-        Log::Warn("SoftBodyInternal has invalid BodyID, skipping body removal from BodyEntityMap");
+        Log::Warn("SoftBodyInternal has invalid BodyID, skipping body addition to BodyEntityMap");
         return;
     }
📝 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
Log::Warn("SoftBodyInternal has invalid BodyID, skipping body removal from BodyEntityMap");
return;
Log::Warn("SoftBodyInternal has invalid BodyID, skipping body addition to BodyEntityMap");
return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` around lines 471 - 472, The
warning in OnSoftBodyInternalConstruct incorrectly says "skipping body removal"
for SoftBodyInternal; update the Log::Warn call in the
OnSoftBodyInternalConstruct handler to state "skipping body addition to
BodyEntityMap" (or similar) so it accurately reflects the construct path and
mentions SoftBodyInternal and BodyEntityMap for clarity; keep the early return
behavior unchanged.

Comment thread src/plugin/physics/src/utils/BiMap.hpp
Comment thread tools/ai/copilot/copilot-commit-message-instructions.md Outdated
@Miou-zora Miou-zora requested a review from Copilot March 18, 2026 21:38

Copilot AI left a comment

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.

Pull request overview

This PR moves the Jolt BodyIDEngine::EntityId association into the physics plugin itself by introducing an internal BodyEntityMap resource backed by a new bidirectional BiMap utility, and wiring map updates into rigid/soft body lifecycle hooks.

Changes:

  • Added Physics::Utils::BiMap<TLeft, TRight> and Physics::Resource::BodyEntityMap (BiMap alias) for bidirectional lookup.
  • Physics plugin now auto-registers BodyEntityMap and updates it from RigidBodySystem / SoftBodySystem callbacks.
  • Added Engine::Entity::Kill(), std::hash<Engine::EntityId>, and new unit tests for mapping + sync.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tools/ai/copilot/copilot-commit-message-instructions.md Adds commit message convention documentation.
src/plugin/physics/tests/BodyEntitySyncTest.cpp New tests validating plugin-driven map synchronization.
src/plugin/physics/tests/BodyEntityMapTest.cpp New unit tests for BodyEntityMap behavior.
src/plugin/physics/src/utils/BiMap.hpp Introduces generic bidirectional map used by BodyEntityMap.
src/plugin/physics/src/system/SoftBodySystem.cpp Updates soft body lifecycle to sync BodyEntityMap.
src/plugin/physics/src/system/RigidBodySystem.cpp Updates rigid body lifecycle to sync BodyEntityMap.
src/plugin/physics/src/resource/BodyEntityMap.hpp Defines the BodyEntityMap resource alias.
src/plugin/physics/src/plugin/PhysicsPlugin.cpp Registers BodyEntityMap as a built-in physics resource.
src/plugin/physics/src/Physics.hpp Exposes new resource/utility in physics umbrella header.
src/engine/src/entity/EntityId.hpp Adds std::hash<Engine::EntityId> for unordered_map usage.
src/engine/src/entity/Entity.hpp Adds Entity::Kill() API.
src/engine/src/entity/Entity.cpp Implements Entity::Kill().
Comments suppressed due to low confidence (1)

src/plugin/physics/src/system/SoftBodySystem.cpp:340

  • corePtr is dereferenced to construct Engine::Entity before checking for nullptr. If the registry context ever contains a null Core* (or is otherwise misconfigured), this will crash before the intended error handling. Check corePtr before using *corePtr.
        auto *corePtr = registry.ctx().get<Engine::Core *>();
        Engine::Entity entity{*corePtr, entityId};

        if (!corePtr)
        {
            Log::Error("Cannot create SoftBody: Engine::Core not available");
            return;
        }
        auto &core = *corePtr;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/plugin/physics/src/utils/BiMap.hpp
Comment thread src/plugin/physics/src/utils/BiMap.hpp
Comment thread src/plugin/physics/src/system/SoftBodySystem.cpp
Comment thread src/plugin/physics/src/system/SoftBodySystem.cpp
Comment thread src/plugin/physics/src/system/RigidBodySystem.cpp
Comment thread src/plugin/physics/tests/BodyEntitySyncTest.cpp
Comment thread src/engine/src/entity/Entity.hpp Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/plugin/physics/src/system/RigidBodySystem.cpp (1)

382-383: Map removal is duplicated across destroy handlers.

OnRigidBodyDestroy removes the BodyID, then entity.RemoveComponent<Component::RigidBodyInternal>() triggers OnRigidBodyInternalDestroy, which removes it again. Keep one path to reduce redundant work and lifecycle coupling.

Also applies to: 391-405

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

In `@src/plugin/physics/src/system/RigidBodySystem.cpp` around lines 382 - 383,
OnRigidBodyDestroy and OnRigidBodyInternalDestroy both call
core->GetResource<Resource::BodyEntityMap>().Remove(internalComponent->bodyID),
causing duplicated removal; pick a single owner of that responsibility (suggest
removing the Remove call from OnRigidBodyDestroy and let removal happen in
OnRigidBodyInternalDestroy which runs when
entity.RemoveComponent<Component::RigidBodyInternal>() is invoked) and update
comments to document the ownership; ensure no other code paths rely on the
removed line and apply the same change for the other duplicated block (the code
around the 391-405 section) so BodyEntityMap::Remove(bodyID) is only called once
per lifecycle.
src/plugin/physics/src/system/SoftBodySystem.cpp (2)

468-468: Avoid copying SoftBodyInternal in hook callbacks.

Use a const reference to avoid copying the component payload.

🧩 Proposed fix
-    auto internalComponent = entity.GetComponents<Component::SoftBodyInternal>();
+    const auto &internalComponent = entity.GetComponents<Component::SoftBodyInternal>();
...
-    auto internalComponent = entity.GetComponents<Component::SoftBodyInternal>();
+    const auto &internalComponent = entity.GetComponents<Component::SoftBodyInternal>();

Also applies to: 518-518

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

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` at line 468, The code is
copying the Component::SoftBodyInternal payload by using auto internalComponent
= entity.GetComponents<Component::SoftBodyInternal>(); in SoftBodySystem.cpp;
change the declaration to bind a const reference (e.g., const auto&
internalComponent or const Component::SoftBodyInternal& internalComponent) so
the hook callbacks don't copy the component data, and apply the same change to
the other occurrence around the 518 call site; ensure you still respect the
component's lifetime when using the reference.

447-447: BodyEntityMap is updated twice via both direct calls and internal hooks.

OnSoftBodyInternalConstruct/OnSoftBodyInternalDestroy already synchronize the map, so explicit Add/Remove in soft-body construct/destroy is redundant.

♻️ Proposed simplification
-        core.GetResource<Resource::BodyEntityMap>().Add(entity, bodyID);
...
-        core.GetResource<Resource::BodyEntityMap>().Remove(internal->bodyID);
         entity.RemoveComponent<Component::SoftBodyInternal>();

Also applies to: 502-503, 540-542

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

In `@src/plugin/physics/src/system/SoftBodySystem.cpp` at line 447, Remove the
redundant explicit BodyEntityMap updates in the soft-body construct/destroy
handlers: delete the core.GetResource<Resource::BodyEntityMap>().Add(...) and
.Remove(...) calls that appear alongside the soft-body construction/destruction
code and rely instead on the existing OnSoftBodyInternalConstruct and
OnSoftBodyInternalDestroy hooks to keep BodyEntityMap in sync; specifically
remove the Add at the construct site and the corresponding Remove(s) at the
destroy sites so only the internal hook methods manage the map.
src/plugin/physics/tests/BodyEntityMapTest.cpp (1)

79-91: Add a reverse-update case (same BodyID, different EntityId).

Please also test Add(EntityA, BodyX) followed by Add(EntityB, BodyX) to validate old left-key eviction on right-key reassignment.

🧪 Proposed extension
 TEST(PhysicsPlugin, BodyEntityMapUpdate)
 {
     Physics::Resource::BodyEntityMap map;

     map.Add(Engine::EntityId{1}, JPH::BodyID{10});
     EXPECT_EQ(map.Get(Engine::EntityId{1}), JPH::BodyID{10});

     map.Add(Engine::EntityId{1}, JPH::BodyID{20});

     EXPECT_EQ(map.Size(), 1);
     EXPECT_EQ(map.Get(Engine::EntityId{1}), JPH::BodyID{20});
     EXPECT_FALSE(map.Contains(JPH::BodyID{10}));
+
+    map.Add(Engine::EntityId{2}, JPH::BodyID{20});
+    EXPECT_EQ(map.Size(), 1);
+    EXPECT_FALSE(map.Contains(Engine::EntityId{1}));
+    EXPECT_EQ(map.Get(JPH::BodyID{20}), Engine::EntityId{2});
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/tests/BodyEntityMapTest.cpp` around lines 79 - 91, Add a
new test case in BodyEntityMapTest that exercises the reverse-update: call
map.Add(Engine::EntityId{1}, JPH::BodyID{10}) then map.Add(Engine::EntityId{2},
JPH::BodyID{10}) and assert the old left-key was evicted and the right-key
reassigned; specifically check map.Size() == 1, map.Get(Engine::EntityId{2}) ==
JPH::BodyID{10}, map no longer returns the old mapping for Engine::EntityId{1}
(e.g. Get(Engine::EntityId{1}) is not JPH::BodyID{10} or is invalid), and
map.Contains(JPH::BodyID{10}) is true.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugin/physics/src/system/RigidBodySystem.cpp`:
- Around line 417-420: The RigidBody event hookups
(registry.on_construct<Component::RigidBody>().connect<&OnRigidBodyConstruct>(),
registry.on_construct<Component::RigidBodyInternal>().connect<&OnRigidBodyInternalConstruct>(),
registry.on_destroy<Component::RigidBody>().connect<&OnRigidBodyDestroy>(),
registry.on_destroy<Component::RigidBodyInternal>().connect<&OnRigidBodyInternalDestroy>())
need matching disconnects on plugin shutdown to avoid callbacks running during
teardown; add a ShutdownRigidBodySystem function that disconnects those four
connections (mirror the pattern used for soft body) and ensure
ShutdownRigidBodySystem is registered in the plugin shutdown schedule so it runs
before physics resources are destroyed.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Around line 332-337: The code dereferences corePtr when constructing
Engine::Entity entity{*corePtr, entityId} before checking for null, which can
crash; move the null check for corePtr (result of
registry.ctx().get<Engine::Core *()> ) before any use of corePtr, and if null
log the error via Log::Error("Cannot create SoftBody: Engine::Core not
available") and return/abort the soft-body creation path instead of constructing
Engine::Entity; update any early-return or error handling in the surrounding
function to match this control flow.

In `@tools/ai/copilot/copilot-commit-message-instructions.md`:
- Line 15: Update the sentence that currently reads "The `<type>` and
`<summary>` fields are mandatory, the `(<scope>)` field is optional." to use the
canonical placeholder `<short summary>` instead of `<summary>` so it matches the
header structure; ensure the line now reads "The `<type>` and `<short summary>`
fields are mandatory, the `(<scope>)` field is optional." and verify any other
occurrences of `<summary>` in this document are replaced with `<short summary>`
for consistency.

---

Nitpick comments:
In `@src/plugin/physics/src/system/RigidBodySystem.cpp`:
- Around line 382-383: OnRigidBodyDestroy and OnRigidBodyInternalDestroy both
call
core->GetResource<Resource::BodyEntityMap>().Remove(internalComponent->bodyID),
causing duplicated removal; pick a single owner of that responsibility (suggest
removing the Remove call from OnRigidBodyDestroy and let removal happen in
OnRigidBodyInternalDestroy which runs when
entity.RemoveComponent<Component::RigidBodyInternal>() is invoked) and update
comments to document the ownership; ensure no other code paths rely on the
removed line and apply the same change for the other duplicated block (the code
around the 391-405 section) so BodyEntityMap::Remove(bodyID) is only called once
per lifecycle.

In `@src/plugin/physics/src/system/SoftBodySystem.cpp`:
- Line 468: The code is copying the Component::SoftBodyInternal payload by using
auto internalComponent = entity.GetComponents<Component::SoftBodyInternal>(); in
SoftBodySystem.cpp; change the declaration to bind a const reference (e.g.,
const auto& internalComponent or const Component::SoftBodyInternal&
internalComponent) so the hook callbacks don't copy the component data, and
apply the same change to the other occurrence around the 518 call site; ensure
you still respect the component's lifetime when using the reference.
- Line 447: Remove the redundant explicit BodyEntityMap updates in the soft-body
construct/destroy handlers: delete the
core.GetResource<Resource::BodyEntityMap>().Add(...) and .Remove(...) calls that
appear alongside the soft-body construction/destruction code and rely instead on
the existing OnSoftBodyInternalConstruct and OnSoftBodyInternalDestroy hooks to
keep BodyEntityMap in sync; specifically remove the Add at the construct site
and the corresponding Remove(s) at the destroy sites so only the internal hook
methods manage the map.

In `@src/plugin/physics/tests/BodyEntityMapTest.cpp`:
- Around line 79-91: Add a new test case in BodyEntityMapTest that exercises the
reverse-update: call map.Add(Engine::EntityId{1}, JPH::BodyID{10}) then
map.Add(Engine::EntityId{2}, JPH::BodyID{10}) and assert the old left-key was
evicted and the right-key reassigned; specifically check map.Size() == 1,
map.Get(Engine::EntityId{2}) == JPH::BodyID{10}, map no longer returns the old
mapping for Engine::EntityId{1} (e.g. Get(Engine::EntityId{1}) is not
JPH::BodyID{10} or is invalid), and map.Contains(JPH::BodyID{10}) is true.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34785713-792d-4e3c-bd22-e3e121c637ff

📥 Commits

Reviewing files that changed from the base of the PR and between 463a291 and fa90bcb.

📒 Files selected for processing (6)
  • src/plugin/physics/src/system/RigidBodySystem.cpp
  • src/plugin/physics/src/system/SoftBodySystem.cpp
  • src/plugin/physics/src/utils/BiMap.hpp
  • src/plugin/physics/tests/BodyEntityMapTest.cpp
  • src/plugin/physics/tests/BodyEntitySyncTest.cpp
  • tools/ai/copilot/copilot-commit-message-instructions.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/plugin/physics/tests/BodyEntitySyncTest.cpp

Comment thread src/plugin/physics/src/system/RigidBodySystem.cpp
Comment thread src/plugin/physics/src/system/SoftBodySystem.cpp
Comment thread tools/ai/copilot/copilot-commit-message-instructions.md Outdated

Copilot AI commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

@Miou-zora I've opened a new pull request, #501, to work on those changes. Once the pull request is ready, I'll request review from you.

Miou-zora and others added 4 commits March 18, 2026 22:51
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Copilot Autofix powered by AI <[email protected]>
…idation (#501)

`BiMap` stored raw pointers to keys inside each `std::unordered_map` as
cross-references. Any insert or erase triggering a rehash invalidated
those pointers, causing use-after-free bugs on lookup and removal.

## Changes

- **`BiMap.hpp`**: Replace pointer-based cross-reference maps with two
independent value-copying maps:
  - `std::unordered_map<TLeft, TRight> _leftToRight`
  - `std::unordered_map<TRight, TLeft> _rightToLeft`
- Update `Add`, `Remove`, `Get`, and `Contains` to work with stored
values directly instead of dereferencing pointers.

**Before:**
```cpp
std::unordered_map<TLeft, const TRight *> _leftToRight;  // pointer into _rightToLeft
std::unordered_map<TRight, const TLeft *> _rightToLeft;  // pointer into _leftToRight

auto aitr = this->_leftToRight.insert_or_assign(left, nullptr);
auto bp = &((*aitr.first).first);           // raw pointer to key — invalidated on rehash
auto bitr = this->_rightToLeft.insert_or_assign(right, bp).first;
auto ap = &((*bitr).first);
(*aitr.first).second = ap;
```

**After:**
```cpp
std::unordered_map<TLeft, TRight> _leftToRight;  // value copy
std::unordered_map<TRight, TLeft> _rightToLeft;  // value copy

this->_leftToRight.insert_or_assign(left, right);
this->_rightToLeft.insert_or_assign(right, left);
```

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Send tasks to Copilot coding agent from
[Slack](https://gh.io/cca-slack-docs) and
[Teams](https://gh.io/cca-teams-docs) to turn conversations into code.
Copilot posts an update in your thread when it's finished.

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Miou-zora <[email protected]>

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
src/plugin/physics/src/utils/BiMap.hpp (1)

49-49: Use std::size_t for Size() to avoid truncation.

Line 49 narrows from container size() to std::uint32_t. Returning std::size_t is safer and matches STL conventions.

Suggested change
-    std::uint32_t Size() const { return static_cast<std::uint32_t>(this->_leftToRight.size()); }
+    std::size_t Size() const { return this->_leftToRight.size(); }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/src/utils/BiMap.hpp` at line 49, The Size() accessor in
class BiMap.hpp currently returns std::uint32_t causing potential truncation;
change its return type to std::size_t and return this->_leftToRight.size()
directly (no narrowing cast). Update the Size() declaration and its definition
(function named Size) to use std::size_t, and then adjust any call-sites that
assumed a uint32_t return type to handle std::size_t if necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/plugin/physics/src/utils/BiMap.hpp`:
- Line 49: The Size() accessor in class BiMap.hpp currently returns
std::uint32_t causing potential truncation; change its return type to
std::size_t and return this->_leftToRight.size() directly (no narrowing cast).
Update the Size() declaration and its definition (function named Size) to use
std::size_t, and then adjust any call-sites that assumed a uint32_t return type
to handle std::size_t if necessary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be8cf709-ec4b-45c3-a909-d9d3aea3b99e

📥 Commits

Reviewing files that changed from the base of the PR and between fa90bcb and cbdbc85.

📒 Files selected for processing (3)
  • src/plugin/physics/src/system/RigidBodySystem.cpp
  • src/plugin/physics/src/system/SoftBodySystem.cpp
  • src/plugin/physics/src/utils/BiMap.hpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/plugin/physics/src/system/SoftBodySystem.cpp
  • src/plugin/physics/src/system/RigidBodySystem.cpp

@sonarqubecloud

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/plugin/physics/tests/PhysicsTest.cpp`:
- Around line 73-78: The assertion for collisionRemoved runs too early inside
the TestScheduler callback; add a second phase that explicitly separates or
destroys the cube and then checks collisionRemoved on a later tick (after the
separation/destruction and after the scheduler processes that change).
Concretely, modify the TestScheduler (registered via
c.RegisterSystem<TestScheduler>) to: 1) keep the existing first-phase
assertions, 2) schedule a follow-up tick or callback that either move the cube's
Transform away or call the cube destruction/removal path, and 3) assert
EXPECT_TRUE(collisionRemoved) only in that follow-up phase (or after
Engine::Scheduler::Shutdown has been processed) so the removal path is
guaranteed to have run.

In `@src/plugin/physics/tests/VehicleForwardMovementTest.cpp`:
- Around line 81-95: The scheduled lambda uses ASSERT_NE for the transform and
controller lookups which returns from the lambda without setting firstStepMade
or stopping the Engine::Core, causing the test to hang; replace those ASSERT_NE
checks with explicit null checks on
vehicle.TryGetComponent<Object::Component::Transform>() and
vehicle.TryGetComponent<Physics::Component::VehicleController>() that, on
failure, call core.Stop() and fail the test (e.g., GTEST_FAIL or FAIL()) before
returning, ensuring firstStepMade is only set after successful setup and
controller->SetForward(1.0f) is only called when components are valid.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 81146391-c85e-4355-86db-5f1cbe873cd0

📥 Commits

Reviewing files that changed from the base of the PR and between cbdbc85 and 40cd9f6.

📒 Files selected for processing (9)
  • src/plugin/physics/src/plugin/PhysicsPlugin.cpp
  • src/plugin/physics/src/system/RigidBodySystem.cpp
  • src/plugin/physics/src/system/RigidBodySystem.hpp
  • src/plugin/physics/src/system/SoftBodySystem.cpp
  • src/plugin/physics/tests/PhysicsTest.cpp
  • src/plugin/physics/tests/VehicleForwardMovementTest.cpp
  • src/plugin/physics/tests/VehicleGravityDropTest.cpp
  • src/plugin/physics/tests/VehicleWheelTransformSyncTest.cpp
  • tools/ai/copilot/copilot-commit-message-instructions.md
✅ Files skipped from review due to trivial changes (1)
  • tools/ai/copilot/copilot-commit-message-instructions.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/plugin/physics/src/system/RigidBodySystem.cpp
  • src/plugin/physics/src/system/SoftBodySystem.cpp

Comment thread src/plugin/physics/tests/PhysicsTest.cpp
Comment on lines +81 to +95
c.RegisterSystem<TestScheduler>([&](Engine::Core &core) {
static bool firstStepMade = false;
if (!firstStepMade)
{
// Record starting position
auto transform = vehicle.TryGetComponent<Object::Component::Transform>();
ASSERT_NE(transform, nullptr);
startPos = transform->GetPosition();

// Apply forward input
auto controller = vehicle.TryGetComponent<Physics::Component::VehicleController>();
ASSERT_NE(controller, nullptr);
controller->SetForward(1.0f); // Full throttle
firstStepMade = 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

Fatal assertions here can hang Engine::Core::RunCore().

If either lookup fails, ASSERT_NE returns from the scheduled lambda, firstStepMade stays false, and the test never reaches core.Stop(). That turns a normal failure into a stuck test process.

🛠️ Fail fast and stop the core on setup errors
-    c.RegisterSystem<TestScheduler>([&](Engine::Core &core) {
-        static bool firstStepMade = false;
+    bool firstStepMade = false;
+    c.RegisterSystem<TestScheduler>([&](Engine::Core &core) {
         if (!firstStepMade)
         {
-            // Record starting position
             auto transform = vehicle.TryGetComponent<Object::Component::Transform>();
-            ASSERT_NE(transform, nullptr);
+            if (transform == nullptr)
+            {
+                ADD_FAILURE() << "Vehicle transform missing";
+                core.Stop();
+                return;
+            }
             startPos = transform->GetPosition();
 
-            // Apply forward input
             auto controller = vehicle.TryGetComponent<Physics::Component::VehicleController>();
-            ASSERT_NE(controller, nullptr);
+            if (controller == nullptr)
+            {
+                ADD_FAILURE() << "Vehicle controller missing";
+                core.Stop();
+                return;
+            }
             controller->SetForward(1.0f); // Full throttle
             firstStepMade = true;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/plugin/physics/tests/VehicleForwardMovementTest.cpp` around lines 81 -
95, The scheduled lambda uses ASSERT_NE for the transform and controller lookups
which returns from the lambda without setting firstStepMade or stopping the
Engine::Core, causing the test to hang; replace those ASSERT_NE checks with
explicit null checks on vehicle.TryGetComponent<Object::Component::Transform>()
and vehicle.TryGetComponent<Physics::Component::VehicleController>() that, on
failure, call core.Stop() and fail the test (e.g., GTEST_FAIL or FAIL()) before
returning, ensuring firstStepMade is only set after successful setup and
controller->SetForward(1.0f) is only called when components are valid.

@Miou-zora Miou-zora merged commit d8edcdd into main Mar 19, 2026
18 checks passed
@Miou-zora Miou-zora deleted the 496-feature-internalize-jolt-bodyid-entity-mapping-inside-the-physics-plugin branch March 19, 2026 10:52
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.

[FEATURE] Internalize Jolt BodyIDEntity mapping inside the Physics plugin

3 participants