Skip to main content

goud_engine/sdk/
entity.rs

1//! # SDK Entity Operations API
2//!
3//! Provides methods on [`GoudGame`](super::GoudGame) for entity lifecycle
4//! management: spawning, despawning, and querying entity state.
5//!
6//! These methods are annotated with `#[goud_api]` to auto-generate FFI
7//! wrappers that replace the hand-written functions in `ffi/entity.rs`.
8
9use super::GoudGame;
10use crate::ecs::Entity;
11
12// NOTE: FFI wrappers are hand-written in ffi/entity.rs. The `#[goud_api]`
13// attribute is omitted here to avoid duplicate `#[no_mangle]` symbol conflicts.
14impl GoudGame {
15    /// Spawns a new empty entity with no components.
16    ///
17    /// Returns the entity as a u64 bit representation for FFI compatibility.
18    pub fn ffi_entity_spawn_empty(&mut self) -> u64 {
19        self.world_mut().spawn_empty().to_bits()
20    }
21
22    /// Spawns multiple empty entities in a batch and writes their IDs to
23    /// the provided output buffer.
24    ///
25    /// Returns the number of entities spawned.
26    ///
27    /// # Safety
28    ///
29    /// `out_entities` must point to valid memory with capacity for at
30    /// least `count` u64 values.
31    pub unsafe fn ffi_entity_spawn_batch(&mut self, count: u32, out_entities: *mut u64) -> u32 {
32        if out_entities.is_null() || count == 0 {
33            return 0;
34        }
35        let entities = self.world_mut().spawn_batch(count as usize);
36        // SAFETY: Caller guarantees out_entities has capacity for count u64s.
37        let out_slice = std::slice::from_raw_parts_mut(out_entities, count as usize);
38        for (i, entity) in entities.iter().enumerate() {
39            out_slice[i] = entity.to_bits();
40        }
41        entities.len() as u32
42    }
43
44    /// Despawns an entity and all its components from the world.
45    ///
46    /// Returns `true` if the entity was despawned, `false` otherwise.
47    pub fn ffi_entity_despawn(&mut self, entity_id: u64) -> bool {
48        let entity = Entity::from_bits(entity_id);
49        self.world_mut().despawn(entity)
50    }
51
52    /// Despawns multiple entities in a batch.
53    ///
54    /// Returns the number of entities successfully despawned.
55    ///
56    /// # Safety
57    ///
58    /// `entity_ids` must point to valid memory with at least `count` u64
59    /// values.
60    pub unsafe fn ffi_entity_despawn_batch(&mut self, entity_ids: *const u64, count: u32) -> u32 {
61        if entity_ids.is_null() || count == 0 {
62            return 0;
63        }
64        // SAFETY: Caller guarantees entity_ids has count u64 values.
65        let entity_slice = std::slice::from_raw_parts(entity_ids, count as usize);
66        let entities: Vec<Entity> = entity_slice
67            .iter()
68            .map(|&bits| Entity::from_bits(bits))
69            .collect();
70        self.world_mut().despawn_batch(&entities) as u32
71    }
72
73    /// Checks if an entity is currently alive in the world.
74    pub fn ffi_entity_is_alive(&self, entity_id: u64) -> bool {
75        let entity = Entity::from_bits(entity_id);
76        self.world().is_alive(entity)
77    }
78
79    /// Returns the total number of alive entities in the world.
80    pub fn ffi_entity_count(&self) -> u32 {
81        self.world().entity_count() as u32
82    }
83
84    /// Checks if multiple entities are alive in the world.
85    ///
86    /// Results are written to `out_results` where 1 = alive, 0 = dead.
87    /// Returns the number of results written.
88    ///
89    /// # Safety
90    ///
91    /// - `entity_ids` must point to valid memory with at least `count` u64
92    ///   values.
93    /// - `out_results` must point to valid memory with at least `count` u8
94    ///   values.
95    pub unsafe fn ffi_entity_is_alive_batch(
96        &self,
97        entity_ids: *const u64,
98        count: u32,
99        out_results: *mut u8,
100    ) -> u32 {
101        if entity_ids.is_null() || out_results.is_null() || count == 0 {
102            return 0;
103        }
104        // SAFETY: Caller guarantees pointers have sufficient capacity.
105        let entity_slice = std::slice::from_raw_parts(entity_ids, count as usize);
106        let results_slice = std::slice::from_raw_parts_mut(out_results, count as usize);
107        for (i, &entity_bits) in entity_slice.iter().enumerate() {
108            let entity = Entity::from_bits(entity_bits);
109            results_slice[i] = if self.world().is_alive(entity) { 1 } else { 0 };
110        }
111        count
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use crate::sdk::GameConfig;
119
120    #[test]
121    fn test_ffi_entity_spawn_empty() {
122        let mut game = GoudGame::new(GameConfig::default()).unwrap();
123        let bits = game.ffi_entity_spawn_empty();
124        assert_ne!(bits, u64::MAX);
125        assert!(game.ffi_entity_is_alive(bits));
126    }
127
128    #[test]
129    fn test_ffi_entity_despawn() {
130        let mut game = GoudGame::new(GameConfig::default()).unwrap();
131        let bits = game.ffi_entity_spawn_empty();
132        assert!(game.ffi_entity_despawn(bits));
133        assert!(!game.ffi_entity_is_alive(bits));
134    }
135
136    #[test]
137    fn test_ffi_entity_count() {
138        let mut game = GoudGame::new(GameConfig::default()).unwrap();
139        assert_eq!(game.ffi_entity_count(), 0);
140        game.ffi_entity_spawn_empty();
141        assert_eq!(game.ffi_entity_count(), 1);
142        game.ffi_entity_spawn_empty();
143        assert_eq!(game.ffi_entity_count(), 2);
144    }
145
146    #[test]
147    fn test_ffi_entity_spawn_batch() {
148        let mut game = GoudGame::new(GameConfig::default()).unwrap();
149        let mut out = vec![0u64; 5];
150        // SAFETY: Test-only call; `out` has capacity for exactly 5 u64 values as required.
151        let count = unsafe { game.ffi_entity_spawn_batch(5, out.as_mut_ptr()) };
152        assert_eq!(count, 5);
153        for &bits in &out {
154            assert!(game.ffi_entity_is_alive(bits));
155        }
156    }
157
158    #[test]
159    fn test_ffi_entity_despawn_batch() {
160        let mut game = GoudGame::new(GameConfig::default()).unwrap();
161        let mut out = vec![0u64; 3];
162        // SAFETY: Test-only call; `out` has capacity for exactly 3 u64 values as required.
163        unsafe { game.ffi_entity_spawn_batch(3, out.as_mut_ptr()) };
164        assert_eq!(game.ffi_entity_count(), 3);
165
166        // SAFETY: Test-only call; `out` contains 3 valid entity IDs from spawn_batch above.
167        let count = unsafe { game.ffi_entity_despawn_batch(out.as_ptr(), 3) };
168        assert_eq!(count, 3);
169        assert_eq!(game.ffi_entity_count(), 0);
170    }
171
172    #[test]
173    fn test_ffi_entity_is_alive_batch() {
174        let mut game = GoudGame::new(GameConfig::default()).unwrap();
175        let mut entities = vec![0u64; 3];
176        // SAFETY: Test-only call; `entities` has capacity for exactly 3 u64 values as required.
177        unsafe { game.ffi_entity_spawn_batch(3, entities.as_mut_ptr()) };
178
179        // Despawn middle one
180        game.ffi_entity_despawn(entities[1]);
181
182        let mut results = vec![0u8; 3];
183        // SAFETY: Test-only call; both `entities` and `results` have capacity for 3 elements.
184        let count =
185            unsafe { game.ffi_entity_is_alive_batch(entities.as_ptr(), 3, results.as_mut_ptr()) };
186        assert_eq!(count, 3);
187        assert_eq!(results[0], 1);
188        assert_eq!(results[1], 0);
189        assert_eq!(results[2], 1);
190    }
191}