Skip to main content

steel_core/entity/
callback.rs

1//! Entity lifecycle callbacks for movement and removal tracking.
2
3use std::sync::Weak;
4
5use glam::DVec3;
6use steel_utils::{ChunkPos, WorldAabb};
7
8use super::EntityMoveError;
9use crate::world::World;
10
11/// Reasons an entity can be removed from the world.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RemovalReason {
14    /// Entity was killed/destroyed.
15    Killed,
16    /// Entity was discarded (e.g., too far from players).
17    Discarded,
18    /// Entity unloaded with chunk.
19    UnloadedToChunk,
20    /// Entity moved to another loaded world.
21    ChangedWorld,
22    /// Entity is persisted inside a player `RootVehicle` payload.
23    StoredWithPlayer,
24}
25
26impl RemovalReason {
27    /// Returns true if entity data should be destroyed (not saved).
28    #[must_use]
29    pub const fn should_destroy(self) -> bool {
30        matches!(self, Self::Killed | Self::Discarded)
31    }
32
33    /// Returns true if the entity should be saved when removed.
34    ///
35    /// In vanilla, only `UnloadedToChunk` saves - the entity persists in chunk storage.
36    /// `ChangedWorld` and `StoredWithPlayer` do not save because the entity
37    /// is retained by another owner instead of current-world entity storage.
38    #[must_use]
39    pub const fn should_save(self) -> bool {
40        matches!(self, Self::UnloadedToChunk)
41    }
42}
43
44/// Callback interface for entity lifecycle events.
45///
46/// Mirrors vanilla's `EntityInLevelCallback`.
47pub trait EntityLevelCallback: Send + Sync {
48    /// Returns whether direct local position writes may bypass lifecycle callbacks.
49    fn allows_local_position_update(&self) -> bool {
50        false
51    }
52
53    /// Called before an entity position change is committed.
54    fn validate_move(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError>;
55
56    /// Called after an entity position change has been committed.
57    fn on_move_committed(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError>;
58
59    /// Called after an entity's collision bounds change without a position change.
60    fn on_bounding_box_changed(&self, _bounding_box: WorldAabb) {}
61
62    /// Called when entity is removed from the world.
63    fn on_remove(&self, reason: RemovalReason);
64}
65
66/// Null callback for entities not yet in the world.
67pub struct NullEntityCallback;
68
69impl EntityLevelCallback for NullEntityCallback {
70    fn allows_local_position_update(&self) -> bool {
71        true
72    }
73
74    fn validate_move(&self, _old_pos: DVec3, _new_pos: DVec3) -> Result<(), EntityMoveError> {
75        Ok(())
76    }
77
78    fn on_move_committed(&self, _old_pos: DVec3, _new_pos: DVec3) -> Result<(), EntityMoveError> {
79        Ok(())
80    }
81
82    fn on_remove(&self, _reason: RemovalReason) {}
83}
84
85/// Callback for entities retained outside live world membership.
86pub struct InactiveEntityCallback {
87    entity_id: i32,
88}
89
90impl InactiveEntityCallback {
91    /// Creates an inactive callback for a retained non-live entity.
92    #[must_use]
93    pub const fn new(entity_id: i32) -> Self {
94        Self { entity_id }
95    }
96}
97
98impl EntityLevelCallback for InactiveEntityCallback {
99    fn validate_move(&self, _old_pos: DVec3, _new_pos: DVec3) -> Result<(), EntityMoveError> {
100        Err(EntityMoveError::Inactive {
101            entity_id: self.entity_id,
102        })
103    }
104
105    fn on_move_committed(&self, _old_pos: DVec3, _new_pos: DVec3) -> Result<(), EntityMoveError> {
106        Err(EntityMoveError::Inactive {
107            entity_id: self.entity_id,
108        })
109    }
110
111    fn on_remove(&self, _reason: RemovalReason) {}
112}
113
114/// Callback for players.
115///
116/// Players are owned by `World.players`, but the world entity manager still
117/// indexes their live position for lookup and tracking updates.
118pub struct PlayerEntityCallback {
119    entity_id: i32,
120    world: Weak<World>,
121}
122
123impl PlayerEntityCallback {
124    /// Creates a new callback for a player.
125    #[must_use]
126    pub const fn new(entity_id: i32, world: Weak<World>) -> Self {
127        Self { entity_id, world }
128    }
129}
130
131impl EntityLevelCallback for PlayerEntityCallback {
132    fn validate_move(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError> {
133        let Some(world) = self.world.upgrade() else {
134            return Err(EntityMoveError::NotLive {
135                entity_id: self.entity_id,
136            });
137        };
138
139        world
140            .entity_manager()
141            .validate_move(self.entity_id, new_pos)
142            .inspect_err(|error| {
143                log::warn!("Rejected player entity move from {old_pos:?} to {new_pos:?}: {error}");
144            })
145    }
146
147    fn on_move_committed(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError> {
148        let Some(world) = self.world.upgrade() else {
149            return Err(EntityMoveError::NotLive {
150                entity_id: self.entity_id,
151            });
152        };
153
154        let update = world
155            .entity_manager()
156            .commit_move(self.entity_id, new_pos)
157            .inspect_err(|error| {
158                log::warn!(
159                    "Failed to commit player entity move from {old_pos:?} to {new_pos:?}: {error}"
160                );
161            })?;
162
163        if update.section_changed() {
164            world.entity_tracker().on_entity_section_change(
165                self.entity_id,
166                update.old_chunk,
167                update.new_chunk,
168                |chunk| world.get_packet_tracking_players(chunk),
169                |player_id| world.players.get_by_entity_id(player_id),
170            );
171
172            if let Some(player) = world.players.get_by_entity_id(self.entity_id)
173                && let Some(view) = *player.last_tracking_view.lock()
174            {
175                let sent_chunks = player.chunk_sender.lock().sent_chunks_snapshot();
176                world
177                    .entity_tracker()
178                    .update_player(&player, &view, |chunk| sent_chunks.contains(&chunk));
179            }
180        }
181
182        Ok(())
183    }
184
185    fn on_bounding_box_changed(&self, _bounding_box: WorldAabb) {
186        if let Some(world) = self.world.upgrade() {
187            world
188                .entity_manager()
189                .commit_bounding_box_change(self.entity_id);
190        }
191    }
192
193    fn on_remove(&self, _reason: RemovalReason) {
194        // Player removal is handled by World::remove_player, not through this callback
195    }
196}
197
198/// Callback attached to each entity for tracking chunk/section movement.
199///
200/// Mirrors vanilla's `PersistentEntitySectionManager.Callback`.
201pub struct EntityChunkCallback {
202    entity_id: i32,
203    world: Weak<World>,
204}
205
206impl EntityChunkCallback {
207    /// Creates a new callback for an entity.
208    #[must_use]
209    pub const fn new(entity_id: i32, world: Weak<World>) -> Self {
210        Self { entity_id, world }
211    }
212}
213
214impl EntityLevelCallback for EntityChunkCallback {
215    fn validate_move(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError> {
216        let Some(world) = self.world.upgrade() else {
217            return Err(EntityMoveError::NotLive {
218                entity_id: self.entity_id,
219            });
220        };
221
222        world
223            .entity_manager()
224            .validate_move(self.entity_id, new_pos)
225            .inspect_err(|error| {
226                log::warn!("Rejected entity move from {old_pos:?} to {new_pos:?}: {error}");
227            })
228    }
229
230    fn on_move_committed(&self, old_pos: DVec3, new_pos: DVec3) -> Result<(), EntityMoveError> {
231        let Some(world) = self.world.upgrade() else {
232            return Err(EntityMoveError::NotLive {
233                entity_id: self.entity_id,
234            });
235        };
236
237        let update = world
238            .entity_manager()
239            .commit_move(self.entity_id, new_pos)
240            .inspect_err(|error| {
241                log::warn!("Failed to commit entity move from {old_pos:?} to {new_pos:?}: {error}");
242            })?;
243
244        world.mark_chunk_dirty(update.new_chunk);
245        if update.chunk_changed() {
246            world.mark_chunk_dirty(update.old_chunk);
247        }
248
249        if update.section_changed() {
250            if update.became_inaccessible() {
251                world.remove_entity_from_tracker(self.entity_id);
252            } else if update.became_accessible() {
253                if let Some(entity) = world.entity_manager().get_by_id(self.entity_id) {
254                    world.add_entity_to_tracker(&entity);
255                }
256            } else if update.new_accessible {
257                world.entity_tracker().on_entity_section_change(
258                    self.entity_id,
259                    update.old_chunk,
260                    update.new_chunk,
261                    |chunk| world.get_packet_tracking_players(chunk),
262                    |player_id| world.players.get_by_entity_id(player_id),
263                );
264            }
265        }
266
267        Ok(())
268    }
269
270    fn on_bounding_box_changed(&self, _bounding_box: WorldAabb) {
271        if let Some(world) = self.world.upgrade() {
272            world
273                .entity_manager()
274                .commit_bounding_box_change(self.entity_id);
275        }
276    }
277
278    fn on_remove(&self, reason: RemovalReason) {
279        let Some(world) = self.world.upgrade() else {
280            return;
281        };
282
283        let entity = world
284            .entity_manager()
285            .remove_live_entity(self.entity_id, reason);
286        if let Some(entity) = entity {
287            world.mark_chunk_dirty(ChunkPos::from_entity_pos(entity.position()));
288        }
289
290        world.remove_entity_from_tracker(self.entity_id);
291    }
292}