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
+126
View File
@@ -0,0 +1,126 @@
use std::collections::HashMap;
use crate::chunk::{Chunk, TileDef, TileFlags, PaletteFullError};
use crate::entity::Entity;
pub fn tile_to_chunk(tx: i16, ty: i16) -> (i16, i16) {
(tx >> 5, ty >> 5)
}
pub fn tile_to_local(tx: i16, ty: i16) -> (u8, u8) {
((tx & 31) as u8, (ty & 31) as u8)
}
pub fn local_to_tile(cx: i16, cy: i16, lx: u8, ly: u8) -> (i16, i16) {
((cx << 5) | lx as i16, (cy << 5) | ly as i16)
}
pub struct World {
chunks: HashMap<(i16, i16), Chunk>,
pub entities: HashMap<u32, Entity>,
chunk_entities: HashMap<(i16, i16), Vec<u32>>,
next_entity_id: u32,
}
impl World {
pub fn new() -> Self {
Self {
chunks: HashMap::new(),
entities: HashMap::new(),
chunk_entities: HashMap::new(),
next_entity_id: 1,
}
}
pub fn chunk(&self, cx: i16, cy: i16) -> Option<&Chunk> {
self.chunks.get(&(cx, cy))
}
pub fn set_chunk(&mut self, cx: i16, cy: i16, chunk: Chunk) {
self.chunks.insert((cx, cy), chunk);
}
pub fn tile_flags(&self, wx: i16, wy: i16) -> Option<TileFlags> {
let (cx, cy) = tile_to_chunk(wx, wy);
let (lx, ly) = tile_to_local(wx, wy);
self.chunks.get(&(cx, cy)).map(|c| c.tile_flags(lx, ly))
}
pub fn set_tile(&mut self, wx: i16, wy: i16, def: TileDef) -> Result<(), PaletteFullError> {
let (cx, cy) = tile_to_chunk(wx, wy);
let (lx, ly) = tile_to_local(wx, wy);
let chunk = self.chunks.entry((cx, cy)).or_insert_with(Chunk::new);
chunk.set_tile(lx, ly, def)
}
pub fn spawn_entity(&mut self, type_id: u16, pos: (i16, i16), hp: u16) -> u32 {
let id = self.next_entity_id;
self.next_entity_id += 1;
let entity = Entity::new(id, type_id, pos, hp);
self.add_entity(entity);
id
}
pub fn add_entity(&mut self, entity: Entity) {
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
let id = entity.id;
self.entities.insert(id, entity);
self.chunk_entities.entry(chunk_key).or_default().push(id);
}
pub fn remove_entity(&mut self, id: u32) {
if let Some(entity) = self.entities.remove(&id) {
let chunk_key = tile_to_chunk(entity.pos.0, entity.pos.1);
if let Some(list) = self.chunk_entities.get_mut(&chunk_key) {
list.retain(|&eid| eid != id);
}
}
}
pub fn move_entity(&mut self, id: u32, new_pos: (i16, i16)) {
if let Some(entity) = self.entities.get_mut(&id) {
let old_chunk = tile_to_chunk(entity.pos.0, entity.pos.1);
let new_chunk = tile_to_chunk(new_pos.0, new_pos.1);
entity.pos = new_pos;
if old_chunk != new_chunk {
if let Some(list) = self.chunk_entities.get_mut(&old_chunk) {
list.retain(|&eid| eid != id);
}
self.chunk_entities.entry(new_chunk).or_default().push(id);
}
}
}
pub fn entities_in_chunk(&self, cx: i16, cy: i16) -> &[u32] {
self.chunk_entities.get(&(cx, cy)).map_or(&[], Vec::as_slice)
}
/// Returns all entities within a 30×30 tile viewport centered on `center`.
pub fn entities_in_viewport(&self, center: (i16, i16)) -> Vec<&Entity> {
let half: i16 = 15;
let min_x = center.0 - half;
let max_x = center.0 + half - 1;
let min_y = center.1 - half;
let max_y = center.1 + half - 1;
let min_cx = tile_to_chunk(min_x, min_y).0;
let max_cx = tile_to_chunk(max_x, max_y).0;
let min_cy = tile_to_chunk(min_x, min_y).1;
let max_cy = tile_to_chunk(max_x, max_y).1;
let mut result = Vec::new();
for cy in min_cy..=max_cy {
for cx in min_cx..=max_cx {
for &eid in self.entities_in_chunk(cx, cy) {
if let Some(e) = self.entities.get(&eid) {
if e.pos.0 >= min_x && e.pos.0 <= max_x
&& e.pos.1 >= min_y && e.pos.1 <= max_y
{
result.push(e);
}
}
}
}
}
result
}
}