use bytemuck::{Pod, Zeroable}; // Game rules and tick constants live in the `sim` crate; re-exported here so the // wire layer and both binaries share one vocabulary. pub use sim::{player_action, action_delta, delta_action, step_allowed, tile_collidable, TICK_HZ, TICKS_PER_MOVE, ACTION_WINDOW_HORIZON}; pub const MAGIC: u16 = 0x524C; pub const VERSION: u8 = 1; pub mod packet_type { pub const STATE: u8 = 0; pub const ACTION: u8 = 1; pub const CHUNK: u8 = 2; pub const ENTITY: u8 = 3; pub const ENTITY_QUERY: u8 = 4; pub const ENTITY_DETAIL: u8 = 5; pub const PING: u8 = 6; pub const PONG: u8 = 7; } /// Encodes signed chunk grid coordinates into the wire chunk_id format. pub fn chunk_id(cx: i16, cy: i16) -> u32 { (cx as u16 as u32) | ((cy as u16 as u32) << 16) } /// Decodes a wire chunk_id back to signed chunk grid coordinates. pub fn chunk_coords(id: u32) -> (i16, i16) { (id as u16 as i16, (id >> 16) as u16 as i16) } #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct Header { pub magic: u16, pub version: u8, pub packet_type: u8, pub client_type: u16, } impl Header { pub fn new(packet_type: u8) -> Self { Self { magic: MAGIC, version: VERSION, packet_type, client_type: 0 } } } #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct ChunkEntry { pub chunk_id: u32, pub version: u16, } #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct ActionPacket { pub header: Header, pub auth_token: u64, /// The server tick this action is scheduled for: it executes in the movement window /// covering that tick, or — if it arrives late — in the next window *if that slot is /// still empty* (late actions fill gaps, they never override newer intent). A second /// action addressed to the same window replaces the first, so a scheduled step can be /// retracted (NOOP) or changed until its window executes. `0` means "no scheduling /// intent": pure keep-alive / cache-ack packets that must never touch the queue. pub target_tick: u32, /// Slot index = (dy+1)*3 + (dx+1), dx/dy ∈ {-1, 0, 1}. /// Slot 4 is always the player's current chunk. pub cache: [ChunkEntry; 9], pub player_action: u16, } const _: () = assert!(std::mem::size_of::() == 6); const _: () = assert!(std::mem::size_of::() == 74); #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct StatePacket { pub header: Header, // 6 (offset 0) pub tick: u32, // 4 (offset 6) /// Slot index = (dy+1)*3 + (dx+1), dx/dy ∈ {-1, 0, 1}. /// Slot 4 is always the player's current chunk. pub chunks: [ChunkEntry; 9], // 54 (offset 10) pub player_entity_id: u32, // 4 (offset 64) pub entity_checksum: u32, // 4 (offset 68) } // total: 72 bytes const _: () = assert!(std::mem::size_of::() == 72); #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct PingPacket { pub header: Header, pub timestamp_ms: u64, } #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct PongPacket { pub header: Header, pub timestamp_ms: u64, } #[derive(Clone, Copy, Pod, Zeroable)] #[repr(C, packed)] pub struct ChunkPacket { pub header: Header, // 6 bytes pub chunk: ChunkEntry, // 6 bytes pub pal_count: u8, // 1 byte pub palette: [u16; 64], // 128 bytes pub tiles: [u8; 768], // 768 bytes — 1024 tiles, 6-bit packed } // total: 909 bytes const _: () = assert!(std::mem::size_of::() == 909); #[repr(C, packed)] #[derive(Clone, Copy, Pod, Zeroable)] pub struct EntityEntry { pub id: u32, pub type_id: u16, pub pos_x: i16, pub pos_y: i16, pub hp: u16, pub hp_max: u16, pub elo: u16, pub entity_flags: u8, pub meta_len: u8, } const _: () = assert!(std::mem::size_of::() == 18); #[repr(C, packed)] #[derive(Clone, Copy, Pod, Zeroable)] pub struct EntityPacket { pub header: Header, pub tick: u32, pub entity_count: u8, pub packet_flags: u8, pub entities: [EntityEntry; 66], } const _: () = assert!(std::mem::size_of::() == 1200);