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:
co-authored by
Claude Opus 5
parent
5610c2c343
commit
303c587aee
Generated
+6
@@ -1477,6 +1477,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"shared",
|
"shared",
|
||||||
|
"sim",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1484,6 +1485,7 @@ name = "shared"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
"sim",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1492,6 +1494,10 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sim"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd_cesu8"
|
name = "simd_cesu8"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["shared", "client", "server", "netsim"]
|
members = ["sim", "shared", "client", "server", "netsim"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
bytemuck = "1.24.0"
|
bytemuck = "1.24.0"
|
||||||
|
sim = { path = "../sim" }
|
||||||
|
|||||||
+2
-33
@@ -1,46 +1,15 @@
|
|||||||
mod chunk;
|
|
||||||
mod entity;
|
|
||||||
mod world;
|
|
||||||
mod sim;
|
|
||||||
mod net;
|
mod net;
|
||||||
mod map;
|
|
||||||
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use chunk::{Chunk, TileDef, TileFlags};
|
use sim::map::TileMap;
|
||||||
use world::World;
|
use sim::{load_world, Sim};
|
||||||
use sim::Sim;
|
|
||||||
use net::Net;
|
use net::Net;
|
||||||
use map::TileMap;
|
|
||||||
|
|
||||||
const TICK_HZ: u32 = 24;
|
const TICK_HZ: u32 = 24;
|
||||||
const TICK_DURATION: Duration = Duration::from_micros(1_000_000 / TICK_HZ as u64);
|
const TICK_DURATION: Duration = Duration::from_micros(1_000_000 / TICK_HZ as u64);
|
||||||
|
|
||||||
/// Build a world from a loaded tile map. The map's top-left tile sits at world (0, 0); the
|
|
||||||
/// map is sliced into 32×32 chunks. Tiles inside a loaded chunk but outside the authored map
|
|
||||||
/// become an invisible solid border (id 0, collidable), so the walkable world edge sits flush
|
|
||||||
/// with the visible map rim instead of with the larger chunk boundary.
|
|
||||||
fn load_world(map: &TileMap) -> World {
|
|
||||||
let mut w = World::new();
|
|
||||||
let max_cx = (map.width.saturating_sub(1) >> 5) as i16;
|
|
||||||
let max_cy = (map.height.saturating_sub(1) >> 5) as i16;
|
|
||||||
for cy in 0..=max_cy {
|
|
||||||
for cx in 0..=max_cx {
|
|
||||||
let chunk = Chunk::generate(|lx, ly| {
|
|
||||||
let (wx, wy) = world::local_to_tile(cx, cy, lx, ly);
|
|
||||||
if wx < 0 || wy < 0 || wx >= map.width as i16 || wy >= map.height as i16 {
|
|
||||||
return TileDef { tile_id: 0, flags: TileFlags(TileFlags::COLLIDABLE) };
|
|
||||||
}
|
|
||||||
let id = map.tile(wx, wy);
|
|
||||||
TileDef { tile_id: id, flags: map::tile_flags(id) }
|
|
||||||
}).expect("palette overflow during world gen");
|
|
||||||
w.set_chunk(cx, cy, chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let map = TileMap::from_csv("assets/map_test");
|
let map = TileMap::from_csv("assets/map_test");
|
||||||
println!("loaded map {}×{} ({} tiles)", map.width, map.height, map.tiles.len());
|
println!("loaded map {}×{} ({} tiles)", map.width, map.height, map.tiles.len());
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ use shared::{
|
|||||||
MAGIC, VERSION, packet_type,
|
MAGIC, VERSION, packet_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::world::World;
|
use sim::world::World;
|
||||||
|
|
||||||
pub struct NetClient {
|
pub struct NetClient {
|
||||||
pub entity_id: u32,
|
pub entity_id: u32,
|
||||||
@@ -153,7 +153,7 @@ impl Net {
|
|||||||
// Step 1 — resolve player position
|
// Step 1 — resolve player position
|
||||||
let Some(entity) = world.entities.get(&client.entity_id) else { continue };
|
let Some(entity) = world.entities.get(&client.entity_id) else { continue };
|
||||||
let pos = entity.pos;
|
let pos = entity.pos;
|
||||||
let (player_cx, player_cy) = crate::world::tile_to_chunk(pos.0, pos.1);
|
let (player_cx, player_cy) = sim::world::tile_to_chunk(pos.0, pos.1);
|
||||||
|
|
||||||
// Step 2 — build 9-slot chunk manifest
|
// Step 2 — build 9-slot chunk manifest
|
||||||
let mut chunks = [ChunkEntry { chunk_id: 0, version: 0 }; 9];
|
let mut chunks = [ChunkEntry { chunk_id: 0, version: 0 }; 9];
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytemuck = { version = "1.24.0", features = ["derive", "min_const_generics"] }
|
bytemuck = { version = "1.24.0", features = ["derive", "min_const_generics"] }
|
||||||
|
sim = { path = "../sim" }
|
||||||
|
|||||||
+4
-90
@@ -1,81 +1,9 @@
|
|||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
pub mod player_action {
|
// Game rules and tick constants live in the `sim` crate; re-exported here so the
|
||||||
pub const NOOP: u16 = 0;
|
// wire layer and both binaries share one vocabulary.
|
||||||
pub const NORTH: u16 = 1;
|
pub use sim::{player_action, action_delta, delta_action, step_allowed, tile_collidable,
|
||||||
pub const EAST: u16 = 2;
|
TICK_HZ, TICKS_PER_MOVE, ACTION_WINDOW_HORIZON};
|
||||||
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 MAGIC: u16 = 0x524C;
|
||||||
pub const VERSION: u8 = 1;
|
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)
|
(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)]
|
#[derive(Clone, Copy, Pod, Zeroable)]
|
||||||
#[repr(C, packed)]
|
#[repr(C, packed)]
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "sim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
//! Headless world simulation: tiles, entities, movement rules and the tick loop.
|
||||||
|
//! No platform or rendering dependencies — the game binary drives `Sim::tick`.
|
||||||
|
|
||||||
|
pub mod chunk;
|
||||||
|
pub mod entity;
|
||||||
|
pub mod map;
|
||||||
|
pub mod world;
|
||||||
|
mod sim;
|
||||||
|
|
||||||
|
pub use sim::Sim;
|
||||||
|
|
||||||
|
use chunk::{Chunk, TileDef, TileFlags};
|
||||||
|
use map::TileMap;
|
||||||
|
use world::World;
|
||||||
|
|
||||||
|
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 sim and the pathfinder so both 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Base tick rate. The 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 an actor may address. Actions targeted beyond
|
||||||
|
/// this horizon are dropped.
|
||||||
|
pub const ACTION_WINDOW_HORIZON: usize = 3;
|
||||||
|
|
||||||
|
/// Movement vocabulary: whether a tile id blocks movement. 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 (chunks past the map rim are padded with id 0)
|
||||||
|
146 => true, // trees / rocks
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a world from a loaded tile map. The map's top-left tile sits at world (0, 0); the
|
||||||
|
/// map is sliced into 32×32 chunks. Tiles inside a loaded chunk but outside the authored map
|
||||||
|
/// become an invisible solid border (id 0, collidable), so the walkable world edge sits flush
|
||||||
|
/// with the visible map rim instead of with the larger chunk boundary.
|
||||||
|
pub fn load_world(map: &TileMap) -> World {
|
||||||
|
let mut w = World::new();
|
||||||
|
let max_cx = (map.width.saturating_sub(1) >> 5) as i16;
|
||||||
|
let max_cy = (map.height.saturating_sub(1) >> 5) as i16;
|
||||||
|
for cy in 0..=max_cy {
|
||||||
|
for cx in 0..=max_cx {
|
||||||
|
let chunk = Chunk::generate(|lx, ly| {
|
||||||
|
let (wx, wy) = world::local_to_tile(cx, cy, lx, ly);
|
||||||
|
if wx < 0 || wy < 0 || wx >= map.width as i16 || wy >= map.height as i16 {
|
||||||
|
return TileDef { tile_id: 0, flags: TileFlags(TileFlags::COLLIDABLE) };
|
||||||
|
}
|
||||||
|
let id = map.tile(wx, wy);
|
||||||
|
TileDef { tile_id: id, flags: map::tile_flags(id) }
|
||||||
|
}).expect("palette overflow during world gen");
|
||||||
|
w.set_chunk(cx, cy, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w
|
||||||
|
}
|
||||||
@@ -65,11 +65,11 @@ impl TileMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Gameplay flags for a global tile id. Collision comes from the shared movement
|
/// Gameplay flags for a global tile id. Collision comes from the shared movement
|
||||||
/// vocabulary (`shared::tile_collidable`) so the client can predict it identically;
|
/// vocabulary (`crate::tile_collidable`) so the client can predict it identically;
|
||||||
/// other flags (opaque, …) stay server-side. Tile ids index into `overworld.tga`.
|
/// other flags (opaque, …) stay sim-side. Tile ids index into `overworld.tga`.
|
||||||
pub fn tile_flags(tile_id: u16) -> TileFlags {
|
pub fn tile_flags(tile_id: u16) -> TileFlags {
|
||||||
let mut bits = 0u8;
|
let mut bits = 0u8;
|
||||||
if shared::tile_collidable(tile_id) {
|
if crate::tile_collidable(tile_id) {
|
||||||
bits |= TileFlags::COLLIDABLE;
|
bits |= TileFlags::COLLIDABLE;
|
||||||
}
|
}
|
||||||
TileFlags(bits)
|
TileFlags(bits)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use shared::{ACTION_WINDOW_HORIZON, TICKS_PER_MOVE};
|
use crate::{ACTION_WINDOW_HORIZON, TICKS_PER_MOVE};
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
|
|
||||||
pub struct Sim {
|
pub struct Sim {
|
||||||
@@ -33,7 +33,7 @@ impl Sim {
|
|||||||
// dropped — once its window has passed, its cancellation intent is
|
// dropped — once its window has passed, its cancellation intent is
|
||||||
// ambiguous, and as a gap-filler it would block real steps (worst case
|
// ambiguous, and as a gap-filler it would block real steps (worst case
|
||||||
// one stale step executes; the client's reconciliation handles that).
|
// one stale step executes; the client's reconciliation handles that).
|
||||||
if shared::action_delta(action).is_some() {
|
if crate::action_delta(action).is_some() {
|
||||||
let gap = (min_window..min_window + ACTION_WINDOW_HORIZON as u32)
|
let gap = (min_window..min_window + ACTION_WINDOW_HORIZON as u32)
|
||||||
.find(|w| !slots.contains_key(w));
|
.find(|w| !slots.contains_key(w));
|
||||||
if let Some(w) = gap {
|
if let Some(w) = gap {
|
||||||
@@ -62,7 +62,7 @@ impl Sim {
|
|||||||
self.pending.retain(|_, slots| !slots.is_empty());
|
self.pending.retain(|_, slots| !slots.is_empty());
|
||||||
|
|
||||||
for (entity_id, action) in actions {
|
for (entity_id, action) in actions {
|
||||||
let delta = match shared::action_delta(action) {
|
let delta = match crate::action_delta(action) {
|
||||||
Some(d) => d,
|
Some(d) => d,
|
||||||
None => continue, // NOOP or garbage
|
None => continue, // NOOP or garbage
|
||||||
};
|
};
|
||||||
@@ -74,7 +74,7 @@ impl Sim {
|
|||||||
let to = (from.0 + delta.0, from.1 + delta.1);
|
let to = (from.0 + delta.0, from.1 + delta.1);
|
||||||
// `step_allowed` is the shared movement rule (chessboard geometry, no corner
|
// `step_allowed` is the shared movement rule (chessboard geometry, no corner
|
||||||
// cutting) — the client predicts with the exact same function.
|
// cutting) — the client predicts with the exact same function.
|
||||||
let allowed = shared::step_allowed(from, to, |x, y| {
|
let allowed = crate::step_allowed(from, to, |x, y| {
|
||||||
self.world.tile_flags(x as i16, y as i16).map_or(true, |f| f.collidable())
|
self.world.tile_flags(x as i16, y as i16).map_or(true, |f| f.collidable())
|
||||||
});
|
});
|
||||||
if allowed {
|
if allowed {
|
||||||
Reference in New Issue
Block a user