Extract headless sim crate from server

Move chunk, entity, world, map, sim and load_world into a new sim library
crate together with the game rules from shared (player_action, action_delta,
delta_action, step_allowed, tile_collidable, tick constants). shared keeps
only the wire types and re-exports the rules, so server and client compile
unchanged. First step toward a local single-player build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irrlicht
2026-09-18 22:52:10 +02:00
co-authored by Claude Opus 5
parent 5610c2c343
commit 303c587aee
14 changed files with 156 additions and 133 deletions
+1
View File
@@ -5,3 +5,4 @@ edition = "2024"
[dependencies]
bytemuck = { version = "1.24.0", features = ["derive", "min_const_generics"] }
sim = { path = "../sim" }
+4 -90
View File
@@ -1,81 +1,9 @@
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 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;
// 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;
@@ -101,20 +29,6 @@ pub fn chunk_coords(id: u32) -> (i16, i16) {
(id as u16 as i16, (id >> 16) as u16 as i16)
}
/// Shared movement vocabulary: whether a tile id blocks movement.
///
/// 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`.
///
/// 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 {
0 => true, // empty / world border (the server pads chunks past the map rim with id 0)
146 => true, // trees / rocks
_ => false,
}
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C, packed)]
pub struct Header {