use crate::coord::EnuConverter;
use crate::net::{
ClientMsg, CompactHole, NetworkTier, PosTracker, ServerMsg, ZoneEvent, decode, encode,
};
use crate::store::ZoneStore;
use crate::zone::ZoneEntry;
pub struct ZoneClient {
pub store: ZoneStore,
pub conv: EnuConverter,
pub entity_id: u32,
pub tier: NetworkTier,
pub tracker: PosTracker,
pub on_enter: Box<dyn Fn(u32) + Send>,
pub on_exit: Box<dyn Fn(u32) + Send>,
pub on_holes: Box<dyn Fn(u32, Vec<CompactHole>) + Send>,
}
impl ZoneClient {
pub fn new(
entity_id: u32,
conv: EnuConverter,
tier: NetworkTier,
) -> Self {
let store = ZoneStore::from_entries(&[], &conv);
Self {
store,
conv,
entity_id,
tier,
tracker: PosTracker::new(entity_id, tier),
on_enter: Box::new(|_| {}),
on_exit: Box::new(|_| {}),
on_holes: Box::new(|_, _| {}),
}
}
pub fn on_server_bytes(&mut self, bytes: &[u8]) -> Option<ServerMsg> {
let msg: ServerMsg = decode(bytes)?;
self.apply_server_msg(&msg);
Some(msg)
}
fn apply_server_msg(&mut self, msg: &ServerMsg) {
match msg {
ServerMsg::ZoneBatch { diffs, .. } => {
for diff in diffs {
match diff {
crate::store::ZoneDiff::Add(e) => {
self.store.add_zone(e.id, &e.zone, &self.conv);
}
crate::store::ZoneDiff::Remove { id } => {
self.store.remove(*id);
}
crate::store::ZoneDiff::Modify { id, zone } => {
self.store.remove(*id);
self.store.add_zone(*id, zone, &self.conv);
}
_ => {}
}
}
}
ServerMsg::EntityEvent { zone_id, event, .. } => match event {
ZoneEvent::Enter => (self.on_enter)(*zone_id),
ZoneEvent::Exit => (self.on_exit)(*zone_id),
_ => {}
},
ServerMsg::ScanResult { zone_id, holes, .. } => {
(self.on_holes)(*zone_id, holes.clone());
}
_ => {}
}
}
pub fn build_pos_msg(&mut self, pos: [f32; 3], ts_ms: u32) -> ClientMsg {
self.tracker.build_pos_msg(pos, ts_ms)
}
pub fn needs_update(&self, actual: [f32; 3], ts_ms: u32) -> bool {
self.tracker.needs_update(actual, ts_ms)
}
pub fn snapshot_request(&self) -> Vec<u8> {
encode(&ClientMsg::RequestSnapshot)
}
pub fn apply_snapshot(&mut self, entries: &[ZoneEntry]) {
self.store = ZoneStore::from_entries(entries, &self.conv);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::ZoneDiff;
use crate::zone::{Zone, ZoneEntry};
fn test_client() -> ZoneClient {
ZoneClient::new(42, EnuConverter::new(0.0, 0.0, 0.0), NetworkTier::Wifi)
}
#[test]
fn apply_zone_batch_adds_and_removes() {
let mut c = test_client();
let msg = ServerMsg::ZoneBatch {
seq: 1,
diffs: vec![
ZoneDiff::Add(ZoneEntry::new(
1,
Zone::Cylinder { center: [0.0, 0.0], radius_m: 10.0, z_min: 0.0, z_max: 5.0 },
)),
ZoneDiff::Add(ZoneEntry::new(
2,
Zone::Cylinder { center: [0.0, 0.0], radius_m: 20.0, z_min: 0.0, z_max: 5.0 },
)),
],
};
let bytes = encode(&msg);
c.on_server_bytes(&bytes);
assert_eq!(c.store.len(), 2);
let remove_msg = ServerMsg::ZoneBatch {
seq: 2,
diffs: vec![ZoneDiff::Remove { id: 1 }],
};
c.on_server_bytes(&encode(&remove_msg));
assert_eq!(c.store.len(), 1);
assert!(c.store.ids().contains(&2));
}
#[test]
fn callbacks_fire_on_events() {
use std::sync::{Arc, Mutex};
let entered = Arc::new(Mutex::new(vec![]));
let exited = Arc::new(Mutex::new(vec![]));
let ent_clone = entered.clone();
let ext_clone = exited.clone();
let mut c = test_client();
c.on_enter = Box::new(move |id| ent_clone.lock().unwrap().push(id));
c.on_exit = Box::new(move |id| ext_clone.lock().unwrap().push(id));
let enter = ServerMsg::EntityEvent {
entity_id: 42,
event: ZoneEvent::Enter,
zone_id: 5,
ts_ms: 100,
};
c.on_server_bytes(&encode(&enter));
assert_eq!(*entered.lock().unwrap(), vec![5]);
let exit = ServerMsg::EntityEvent {
entity_id: 42,
event: ZoneEvent::Exit,
zone_id: 5,
ts_ms: 200,
};
c.on_server_bytes(&encode(&exit));
assert_eq!(*exited.lock().unwrap(), vec![5]);
}
#[test]
fn build_pos_and_snapshot() {
let mut c = test_client();
let msg = c.build_pos_msg([1.0, 2.0, 3.0], 50);
assert!(matches!(msg, ClientMsg::DeltaPos { .. }));
let msg2 = c.build_pos_msg([500.0, 2.0, 3.0], 100);
assert!(matches!(msg2, ClientMsg::FullPos { .. }));
let snap = c.snapshot_request();
let decoded: ClientMsg = decode(&snap).unwrap();
assert!(matches!(decoded, ClientMsg::RequestSnapshot));
}
}