1use crate::shared_world::SabSlot;
7use aetheris_protocol::error::WorldError;
8use aetheris_protocol::events::{ComponentUpdate, ReplicationEvent};
9use aetheris_protocol::traits::WorldState;
10use aetheris_protocol::types::{
11 ClientId, ComponentKind, LocalId, NetworkId, ShipClass, ShipStats, Transform,
12};
13use std::collections::BTreeMap;
14
15#[derive(Debug)]
17pub struct ClientWorld {
18 pub entities: BTreeMap<NetworkId, SabSlot>,
20 pub player_network_id: Option<NetworkId>,
22 pub latest_tick: u64,
24 pub system_manifest: BTreeMap<String, String>,
26 pub shared_world_ref: Option<usize>,
28}
29
30impl Default for ClientWorld {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl ClientWorld {
37 #[must_use]
39 pub fn new() -> Self {
40 Self {
41 entities: BTreeMap::new(),
42 player_network_id: None,
43 latest_tick: 0,
44 system_manifest: BTreeMap::new(),
45 shared_world_ref: None,
46 }
47 }
48}
49
50impl WorldState for ClientWorld {
51 fn get_local_id(&self, network_id: NetworkId) -> Option<LocalId> {
52 Some(LocalId(network_id.0))
53 }
54
55 fn get_network_id(&self, local_id: LocalId) -> Option<NetworkId> {
56 Some(NetworkId(local_id.0))
57 }
58
59 fn extract_deltas(&mut self) -> Vec<ReplicationEvent> {
60 Vec::new()
61 }
62
63 fn apply_updates(&mut self, updates: &[(ClientId, ComponentUpdate)]) {
64 if !updates.is_empty() {
65 tracing::debug!(
66 count = updates.len(),
67 player_network_id = ?self.player_network_id,
68 total_entities = self.entities.len(),
69 "[apply_updates] Processing updates batch"
70 );
71 }
72 for (_, update) in updates {
73 if update.tick > self.latest_tick {
74 self.latest_tick = update.tick;
75 }
76
77 let is_new = !self.entities.contains_key(&update.network_id);
78
79 let entry = self.entities.entry(update.network_id).or_insert_with(|| {
81 tracing::info!(
82 network_id = update.network_id.0,
83 kind = update.component_kind.0,
84 player_network_id = ?self.player_network_id,
85 "[apply_updates] NEW entity from server"
86 );
87 SabSlot {
88 network_id: update.network_id.0,
89 x: 0.0,
90 y: 0.0,
91 z: 0.0,
92 rotation: 0.0,
93 dx: 0.0,
94 dy: 0.0,
95 dz: 0.0,
96 hp: 100,
97 shield: 0,
98 entity_type: 0,
99 flags: 1,
100 cargo_ore: 0,
101 mining_target_id: 0,
102 mining_active: 0,
103 }
104 });
105
106 let is_player = Some(update.network_id) == self.player_network_id;
109 if is_player {
110 tracing::info!(
111 network_id = update.network_id.0,
112 is_new,
113 "[apply_updates] Setting 0x04 (LocalPlayer) flag on entity"
114 );
115 entry.flags |= 0x04;
116 } else if is_new {
117 tracing::info!(
118 network_id = update.network_id.0,
119 player_network_id = ?self.player_network_id,
120 flags = entry.flags,
121 "[apply_updates] New NON-player entity - no possession flag"
122 );
123 }
124
125 self.apply_component_update(update);
126 }
127 }
128
129 fn simulate(&mut self) {
130 let dt = 0.05; let drag_base = 0.05;
132 for slot in self.entities.values_mut() {
133 slot.x += slot.dx * dt;
135 slot.y += slot.dy * dt;
136
137 slot.dx *= 1.0 - (drag_base * dt);
139 slot.dy *= 1.0 - (drag_base * dt);
140 }
141 }
142
143 fn spawn_networked(&mut self) -> NetworkId {
144 NetworkId(0)
145 }
146
147 fn spawn_networked_for(&mut self, _client_id: ClientId) -> NetworkId {
148 self.spawn_networked()
149 }
150
151 fn despawn_networked(&mut self, network_id: NetworkId) -> Result<(), WorldError> {
152 self.entities
153 .remove(&network_id)
154 .map(|_| ())
155 .ok_or(WorldError::EntityNotFound(network_id))
156 }
157
158 fn stress_test(&mut self, _count: u16, _rotate: bool) {}
159
160 fn spawn_kind(&mut self, _kind: u16, _x: f32, _y: f32, _rot: f32) -> NetworkId {
161 NetworkId(1)
162 }
163
164 fn clear_world(&mut self) {
165 self.entities.clear();
166 }
167
168 fn state_hash(&self) -> u64 {
169 use std::hash::{Hash, Hasher};
170 use twox_hash::XxHash64;
171
172 let mut hasher = XxHash64::with_seed(0);
174 self.latest_tick.hash(&mut hasher);
175
176 for (nid, slot) in &self.entities {
178 nid.hash(&mut hasher);
179
180 slot.x.to_bits().hash(&mut hasher);
182 slot.y.to_bits().hash(&mut hasher);
183 slot.z.to_bits().hash(&mut hasher);
184 slot.rotation.to_bits().hash(&mut hasher);
185
186 slot.dx.to_bits().hash(&mut hasher);
188 slot.dy.to_bits().hash(&mut hasher);
189 slot.dz.to_bits().hash(&mut hasher);
190
191 slot.hp.hash(&mut hasher);
192 slot.shield.hash(&mut hasher);
193 slot.entity_type.hash(&mut hasher);
194 slot.flags.hash(&mut hasher);
195
196 slot.mining_active.hash(&mut hasher);
197 slot.cargo_ore.hash(&mut hasher);
198 slot.mining_target_id.hash(&mut hasher);
199 }
200
201 hasher.finish()
202 }
203}
204
205impl ClientWorld {
206 pub fn handle_game_event(&mut self, event: &aetheris_protocol::events::GameEvent) {
208 if let aetheris_protocol::events::GameEvent::Possession { network_id } = event {
209 let prev = self.player_network_id;
210 tracing::info!(
211 ?network_id,
212 ?prev,
213 entity_exists = self.entities.contains_key(network_id),
214 total_entities = self.entities.len(),
215 "[handle_game_event] POSSESSION received"
216 );
217 if let Some(slot) = prev
219 .filter(|&id| id != *network_id)
220 .and_then(|id| self.entities.get_mut(&id))
221 {
222 slot.flags &= !0x04;
223 tracing::info!(
224 network_id = ?prev,
225 flags = slot.flags,
226 "[handle_game_event] 0x04 flag cleared from previous entity"
227 );
228 }
229 self.player_network_id = Some(*network_id);
230 if let Some(slot) = self.entities.get_mut(network_id) {
231 slot.flags |= 0x04;
232 tracing::info!(
233 ?network_id,
234 flags = slot.flags,
235 "[handle_game_event] 0x04 flag applied to entity"
236 );
237 } else {
238 tracing::warn!(
239 ?network_id,
240 "[handle_game_event] Possession entity not yet in world - will apply when it arrives"
241 );
242 }
243 }
244 }
245
246 fn apply_component_update(&mut self, update: &ComponentUpdate) {
247 match update.component_kind {
248 ComponentKind(1) => match rmp_serde::from_slice::<Transform>(&update.payload) {
250 Ok(transform) => {
251 if let Some(entry) = self.entities.get_mut(&update.network_id) {
252 entry.x = transform.x;
253 entry.y = transform.y;
254 entry.z = transform.z;
255 entry.rotation = transform.rotation;
256 if transform.entity_type != 0 {
257 entry.entity_type = transform.entity_type;
258 }
259 }
260 }
261 Err(e) => {
262 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode Transform");
263 }
264 },
265 ComponentKind(5) => match rmp_serde::from_slice::<ShipClass>(&update.payload) {
267 Ok(ship_class) => {
268 if let Some(entry) = self.entities.get_mut(&update.network_id) {
269 entry.entity_type = match ship_class {
270 ShipClass::Interceptor => 1,
271 ShipClass::Dreadnought => 3,
272 ShipClass::Hauler => 4,
273 };
274 }
275 }
276 Err(e) => {
277 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode ShipClass");
278 }
279 },
280 ComponentKind(3) => match rmp_serde::from_slice::<ShipStats>(&update.payload) {
282 Ok(stats) => {
283 if let Some(entry) = self.entities.get_mut(&update.network_id) {
284 entry.hp = stats.hp;
285 entry.shield = stats.shield;
286 }
287 }
288 Err(e) => {
289 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode ShipStats");
290 }
291 },
292 aetheris_protocol::types::MINING_BEAM_KIND => {
294 use aetheris_protocol::types::MiningBeam;
295 match rmp_serde::from_slice::<MiningBeam>(&update.payload) {
296 Ok(beam) => {
297 if let Some(entry) = self.entities.get_mut(&update.network_id) {
298 entry.mining_active = u8::from(beam.active);
299 #[allow(clippy::cast_possible_truncation)]
300 {
301 entry.mining_target_id = beam.target.map_or(0, |id| id.0 as u16);
302 }
303 }
304 }
305 Err(e) => {
306 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode MiningBeam");
307 }
308 }
309 }
310 aetheris_protocol::types::CARGO_HOLD_KIND => {
312 use aetheris_protocol::types::CargoHold;
313 match rmp_serde::from_slice::<CargoHold>(&update.payload) {
314 Ok(cargo) => {
315 if let Some(entry) = self.entities.get_mut(&update.network_id) {
316 entry.cargo_ore = cargo.ore_count;
317 entry.flags |= 0x04;
320 }
321 }
322 Err(e) => {
323 tracing::warn!(network_id = update.network_id.0, error = ?e, "Failed to decode CargoHold");
324 }
325 }
326 }
327 aetheris_protocol::types::ASTEROID_KIND => {
329 if let Some(entry) = self.entities.get_mut(&update.network_id) {
330 if entry.entity_type == 0 {
332 entry.entity_type = 5;
333 }
334 }
335 }
336 aetheris_protocol::types::ROOM_BOUNDS_KIND => {
338 use aetheris_protocol::types::RoomBounds;
339 if let (Ok(bounds), Some(ptr_val)) = (
340 rmp_serde::from_slice::<RoomBounds>(&update.payload),
341 self.shared_world_ref,
342 ) {
343 let mut sw =
344 unsafe { crate::shared_world::SharedWorld::from_ptr(ptr_val as *mut u8) };
345 sw.set_room_bounds(bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y);
346 }
347 }
348 kind => {
349 tracing::debug!(
350 network_id = update.network_id.0,
351 kind = kind.0,
352 "Unhandled component kind"
353 );
354 }
355 }
356 }
357}
358
359impl ClientWorld {
360 pub fn playground_apply_input(&mut self, move_x: f32, move_y: f32, actions_mask: u32) {
363 const THRUST_ACCEL: f32 = 0.12;
365 const DRAG: f32 = 0.92;
366 const MAX_SPEED: f32 = 3.0;
367
368 for slot in self.entities.values_mut() {
370 if (slot.flags & 0x04) != 0 {
371 slot.dx = (slot.dx + move_x * THRUST_ACCEL) * DRAG;
373 slot.dy = (slot.dy + move_y * THRUST_ACCEL) * DRAG;
374
375 let speed_sq = slot.dx * slot.dx + slot.dy * slot.dy;
377 if speed_sq > MAX_SPEED * MAX_SPEED {
378 let speed = speed_sq.sqrt();
379 slot.dx = (slot.dx / speed) * MAX_SPEED;
380 slot.dy = (slot.dy / speed) * MAX_SPEED;
381 }
382
383 slot.x += slot.dx;
385 slot.y += slot.dy;
386
387 if (actions_mask & 0x02) != 0 {
389 slot.mining_active = 0;
390 slot.mining_target_id = 0;
391 }
392
393 break;
394 }
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::shared_world::SabSlot;
403 use aetheris_protocol::types::NetworkId;
404 use bytemuck::Zeroable;
405
406 #[test]
407 fn test_playground_movement() {
408 let mut world = ClientWorld::new();
409
410 world.entities.insert(
412 NetworkId(1),
413 SabSlot {
414 network_id: 1,
415 flags: 0x04, ..SabSlot::zeroed()
417 },
418 );
419
420 world.playground_apply_input(1.0, 0.0, 0);
422
423 let player = world.entities.get(&NetworkId(1)).unwrap();
424 assert!(player.dx > 0.0);
425 assert!(player.x > 0.0);
426 assert!((player.dx - 0.1104).abs() < 0.0001);
428 }
429
430 #[test]
431 fn test_playground_speed_clamp() {
432 let mut world = ClientWorld::new();
433 world.entities.insert(
434 NetworkId(1),
435 SabSlot {
436 network_id: 1,
437 flags: 0x04,
438 dx: 10.0,
439 dy: 10.0,
440 ..SabSlot::zeroed()
441 },
442 );
443
444 world.playground_apply_input(1.0, 1.0, 0);
446
447 let player = world.entities.get(&NetworkId(1)).unwrap();
448 let speed = (player.dx * player.dx + player.dy * player.dy).sqrt();
449 assert!(speed <= 3.0 + 0.0001);
450 }
451
452 #[test]
453 fn test_playground_drag() {
454 let mut world = ClientWorld::new();
455 world.entities.insert(
456 NetworkId(1),
457 SabSlot {
458 network_id: 1,
459 flags: 0x04,
460 ..SabSlot::zeroed()
461 },
462 );
463
464 world.playground_apply_input(1.0, 0.0, 0);
466 let v1 = world.entities.get(&NetworkId(1)).unwrap().dx;
467
468 world.playground_apply_input(0.0, 0.0, 0);
470 let v2 = world.entities.get(&NetworkId(1)).unwrap().dx;
471
472 assert!(v2 < v1);
473 assert!((v2 - v1 * 0.92).abs() < 0.0001);
474 }
475}