current state
This commit is contained in:
+86
-9
@@ -1,13 +1,82 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
pub mod player_action {
|
||||
pub const NOOP: u16 = 0;
|
||||
pub const NORTH: u16 = 1;
|
||||
pub const EAST: u16 = 2;
|
||||
pub const SOUTH: u16 = 3;
|
||||
pub const WEST: u16 = 4;
|
||||
pub const NOOP: u16 = 0;
|
||||
pub const NORTH: u16 = 1;
|
||||
pub const EAST: u16 = 2;
|
||||
pub const SOUTH: u16 = 3;
|
||||
pub const WEST: u16 = 4;
|
||||
pub const NORTH_EAST: u16 = 5;
|
||||
pub const SOUTH_EAST: u16 = 6;
|
||||
pub const SOUTH_WEST: u16 = 7;
|
||||
pub const NORTH_WEST: u16 = 8;
|
||||
}
|
||||
|
||||
/// The movement delta of an action, or `None` for `NOOP` and unknown values.
|
||||
pub fn action_delta(action: u16) -> Option<(i32, i32)> {
|
||||
match action {
|
||||
player_action::NORTH => Some((0, -1)),
|
||||
player_action::EAST => Some((1, 0)),
|
||||
player_action::SOUTH => Some((0, 1)),
|
||||
player_action::WEST => Some((-1, 0)),
|
||||
player_action::NORTH_EAST => Some((1, -1)),
|
||||
player_action::SOUTH_EAST => Some((1, 1)),
|
||||
player_action::SOUTH_WEST => Some((-1, 1)),
|
||||
player_action::NORTH_WEST => Some((-1, -1)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The action for a single-step delta. Inverse of [`action_delta`]; panics on anything
|
||||
/// that is not a king move.
|
||||
pub fn delta_action(dx: i32, dy: i32) -> u16 {
|
||||
match (dx, dy) {
|
||||
(0, -1) => player_action::NORTH,
|
||||
(1, 0) => player_action::EAST,
|
||||
(0, 1) => player_action::SOUTH,
|
||||
(-1, 0) => player_action::WEST,
|
||||
(1, -1) => player_action::NORTH_EAST,
|
||||
(1, 1) => player_action::SOUTH_EAST,
|
||||
(-1, 1) => player_action::SOUTH_WEST,
|
||||
(-1, -1) => player_action::NORTH_WEST,
|
||||
d => panic!("non-step delta {d:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The single-step movement rule, shared by the server sim, client prediction and the
|
||||
/// client pathfinder so all three agree on the same physics.
|
||||
///
|
||||
/// World geometry is chessboard (Chebyshev): diagonal and cardinal steps are the same
|
||||
/// distance, so a step is any king move onto a free tile. A diagonal step additionally
|
||||
/// requires *both* orthogonal neighbor tiles to be free — no squeezing between two
|
||||
/// diagonally touching blockers (corner cutting).
|
||||
pub fn step_allowed(
|
||||
from: (i32, i32),
|
||||
to: (i32, i32),
|
||||
blocked: impl Fn(i32, i32) -> bool,
|
||||
) -> bool {
|
||||
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
|
||||
if dx.abs() > 1 || dy.abs() > 1 || (dx == 0 && dy == 0) {
|
||||
return false;
|
||||
}
|
||||
if blocked(to.0, to.1) {
|
||||
return false;
|
||||
}
|
||||
dx == 0 || dy == 0 || (!blocked(from.0 + dx, from.1) && !blocked(from.0, from.1 + dy))
|
||||
}
|
||||
|
||||
/// Server base tick rate. The shared timeline all action scheduling is expressed in.
|
||||
pub const TICK_HZ: u32 = 24;
|
||||
|
||||
/// Movement resolves on every `TICKS_PER_MOVE`-th tick — one "movement window" per
|
||||
/// `TICKS_PER_MOVE` ticks (6 Hz). Window `w` executes at tick `w * TICKS_PER_MOVE`.
|
||||
pub const TICKS_PER_MOVE: u32 = 4;
|
||||
|
||||
/// How many *future* movement windows a client may address. Part of the protocol
|
||||
/// contract: actions targeted beyond this horizon are dropped, and the client's
|
||||
/// unconfirmed in-flight steps must stay within it.
|
||||
pub const ACTION_WINDOW_HORIZON: usize = 3;
|
||||
|
||||
pub const MAGIC: u16 = 0x524C;
|
||||
pub const VERSION: u8 = 1;
|
||||
|
||||
@@ -36,11 +105,13 @@ pub fn chunk_coords(id: u32) -> (i16, i16) {
|
||||
///
|
||||
/// Single source of truth for both the server (collision in the sim) and the client
|
||||
/// (movement prediction). Keep this in lockstep with the tileset in `overworld.tga`.
|
||||
/// Currently every authored tile is walkable.
|
||||
///
|
||||
/// Test values for now — a proper tile-data file format replaces this table later.
|
||||
pub fn tile_collidable(tile_id: u16) -> bool {
|
||||
match tile_id {
|
||||
// e.g. 146 => true, // trees / rocks
|
||||
_ => false,
|
||||
0 => true, // empty / world border (the server pads chunks past the map rim with id 0)
|
||||
146 => true, // trees / rocks
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +142,13 @@ pub struct ChunkEntry {
|
||||
pub struct ActionPacket {
|
||||
pub header: Header,
|
||||
pub auth_token: u64,
|
||||
pub sequence: u32,
|
||||
/// 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],
|
||||
|
||||
Reference in New Issue
Block a user