Skip to main content

khora_data/ecs/systems/
physics_world_writeback.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Pulls per-body transforms, kinematic results, and collision events
16//! from the physics provider back into the ECS World.
17//!
18//! Replaces `StandardPhysicsLane::sync_from_world` /
19//! `resolve_characters` / `dispatch_events` (which queried the World
20//! directly inside the Lane and violated the CLAD rule). Runs in
21//! `Maintenance` phase, after the scheduler has executed the physics
22//! agent and the lane has called `provider.step(dt)`.
23
24use std::sync::{Arc, Mutex};
25
26use khora_core::ecs::entity::EntityId;
27use khora_core::lane::OutputDeck;
28use khora_core::physics::{CharacterControllerOptions, PhysicsProvider};
29use khora_core::Runtime;
30
31use crate::ecs::{
32    Collider, CollisionEvents, DataSystemRegistration, KinematicCharacterController, RigidBody,
33    TickPhase, Transform, World,
34};
35use crate::flow::PhysicsStepResult;
36
37fn physics_world_writeback(world: &mut World, runtime: &Runtime, deck: &mut OutputDeck) {
38    // Only writeback if the physics lane actually advanced the simulation
39    // this frame (it publishes a `PhysicsStepResult` slot when it ran).
40    // When the agent is paused, throttled, or the lane was skipped for
41    // any reason, we don't want to pull stale provider state.
42    if !deck.contains::<PhysicsStepResult>() {
43        return;
44    }
45    let _step = deck.take::<PhysicsStepResult>();
46
47    let Some(provider_arc) = runtime
48        .backends
49        .get::<Arc<Mutex<Box<dyn PhysicsProvider>>>>()
50        .cloned()
51    else {
52        return;
53    };
54    let guard = match provider_arc.lock() {
55        Ok(g) => g,
56        Err(e) => {
57            log::error!("physics_world_writeback: provider mutex poisoned: {}", e);
58            return;
59        }
60    };
61    let provider = guard.as_ref();
62
63    sync_from_provider(world, provider);
64    resolve_characters(world, provider);
65    dispatch_collision_events(world, provider);
66}
67
68/// Pull body transforms from the provider into Transform / RigidBody.
69fn sync_from_provider(world: &mut World, provider: &dyn PhysicsProvider) {
70    for (transform, rb) in world.query_mut::<(&mut Transform, &mut RigidBody)>() {
71        if let Some(handle) = rb.handle {
72            let (pos, rot) = provider.get_body_transform(handle);
73            transform.translation = pos;
74            transform.rotation = rot;
75        }
76    }
77}
78
79/// Apply kinematic-character-controller movement results.
80fn resolve_characters(world: &mut World, provider: &dyn PhysicsProvider) {
81    let mut results = Vec::new();
82    {
83        let query = world.query_mut::<(EntityId, &mut KinematicCharacterController, &Collider)>();
84        for (id, kcc, collider) in query {
85            if let Some(h) = collider.handle {
86                let options = CharacterControllerOptions {
87                    autostep_height: kcc.autostep_height,
88                    autostep_min_width: kcc.autostep_min_width,
89                    autostep_enabled: kcc.autostep_enabled,
90                    max_slope_climb_angle: kcc.max_slope_climb_angle,
91                    min_slope_slide_angle: kcc.min_slope_slide_angle,
92                    offset: kcc.offset,
93                };
94                let (m, g) = provider.move_character(h, kcc.desired_translation, &options);
95                results.push((id, m, g));
96            }
97        }
98    }
99
100    for (id, m, g) in results {
101        if let Some(kcc) = world.get_mut::<KinematicCharacterController>(id) {
102            kcc.is_grounded = g;
103            kcc.desired_translation = khora_core::math::Vec3::ZERO;
104        }
105        if let Some(transform) = world.get_mut::<Transform>(id) {
106            transform.translation = transform.translation + m;
107        }
108    }
109}
110
111/// Mirror the provider's collision-event buffer into every entity that
112/// declared a `CollisionEvents` component.
113fn dispatch_collision_events(world: &mut World, provider: &dyn PhysicsProvider) {
114    let events = provider.get_collision_events();
115    for (_, buffer) in world.query_mut::<(EntityId, &mut CollisionEvents)>() {
116        if events.is_empty() {
117            buffer.events.clear();
118        } else {
119            buffer.events = events.clone();
120        }
121    }
122}
123
124inventory::submit! {
125    DataSystemRegistration {
126        name: "physics_world_writeback",
127        phase: TickPhase::Maintenance,
128        run: physics_world_writeback,
129        order_hint: 0,
130        runs_after: &[],
131    }
132}