Drop the chunk palette; chunks are plain u16 tile arrays
The 64-entry per-chunk palette was a wire-format artifact and capped a chunk at 64 distinct tiles. A Chunk is now Box<[u16; 1024]> with flags derived from the id on lookup — the same layout a binary on-disk format would use. Map path becomes the first CLI argument; assets/map_large is a generated 256×256 stress map (loads in ~0.5 ms release). The snapshot test takes SNAPSHOT_MAP. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5a055c048d
commit
1d14076468
+21
-70
@@ -1,3 +1,4 @@
|
||||
/// Gameplay properties of a tile, derived from its id (see `map::tile_flags`).
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct TileFlags(pub u8);
|
||||
|
||||
@@ -11,92 +12,42 @@ impl TileFlags {
|
||||
pub fn liquid(self) -> bool { self.0 & Self::LIQUID != 0 }
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||
pub struct TileDef { pub tile_id: u16, pub flags: TileFlags }
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PaletteFullError;
|
||||
pub const CHUNK_SIZE: usize = 32;
|
||||
pub const CHUNK_TILES: usize = CHUNK_SIZE * CHUNK_SIZE;
|
||||
|
||||
/// A 32×32 block of tile ids, row-major. This is the whole chunk: flags are derived from
|
||||
/// the id on lookup, so the in-memory layout is already the plain `u16` array a binary
|
||||
/// on-disk format would store.
|
||||
pub struct Chunk {
|
||||
pub palette: [TileDef; 64],
|
||||
pub pal_count: u8,
|
||||
pub tiles: Box<[u8; 1024]>,
|
||||
pub version: u16,
|
||||
pub tiles: Box<[u16; CHUNK_TILES]>,
|
||||
}
|
||||
|
||||
impl Chunk {
|
||||
/// Runtime use: all tiles empty, pal_count=1, version=1.
|
||||
/// Clients with version=0 will always request this chunk.
|
||||
/// All tiles id 0 (empty / solid border).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
palette: [TileDef::default(); 64],
|
||||
pal_count: 1,
|
||||
tiles: Box::new([0u8; 1024]),
|
||||
version: 1,
|
||||
}
|
||||
Self { tiles: Box::new([0; CHUNK_TILES]) }
|
||||
}
|
||||
|
||||
/// Find existing palette entry or insert a new one.
|
||||
/// Index 0 is always the null sentinel; this short-circuits for default TileDef.
|
||||
/// Returns (palette_index, was_new). Does NOT bump version.
|
||||
fn find_or_insert_raw(&mut self, def: TileDef) -> Result<(u8, bool), PaletteFullError> {
|
||||
if def == TileDef::default() {
|
||||
return Ok((0, false));
|
||||
}
|
||||
for i in 1..self.pal_count as usize {
|
||||
if self.palette[i] == def {
|
||||
return Ok((i as u8, false));
|
||||
/// World gen path. Calls the closure for every local tile coordinate.
|
||||
pub fn generate(f: impl Fn(u8, u8) -> u16) -> Self {
|
||||
let mut chunk = Self::new();
|
||||
for ly in 0..CHUNK_SIZE {
|
||||
for lx in 0..CHUNK_SIZE {
|
||||
chunk.tiles[ly * CHUNK_SIZE + lx] = f(lx as u8, ly as u8);
|
||||
}
|
||||
}
|
||||
if self.pal_count >= 64 {
|
||||
return Err(PaletteFullError);
|
||||
}
|
||||
let idx = self.pal_count;
|
||||
self.palette[idx as usize] = def;
|
||||
self.pal_count += 1;
|
||||
Ok((idx, true))
|
||||
chunk
|
||||
}
|
||||
|
||||
/// World gen path. Calls the closure for every tile coordinate.
|
||||
/// version=0 so the first StatePacket causes all clients to request it.
|
||||
pub fn generate(f: impl Fn(u8, u8) -> TileDef) -> Result<Self, PaletteFullError> {
|
||||
let mut chunk = Self {
|
||||
palette: [TileDef::default(); 64],
|
||||
pal_count: 1,
|
||||
tiles: Box::new([0u8; 1024]),
|
||||
version: 0,
|
||||
};
|
||||
for ly in 0u8..32 {
|
||||
for lx in 0u8..32 {
|
||||
let def = f(lx, ly);
|
||||
let (idx, _) = chunk.find_or_insert_raw(def)?;
|
||||
chunk.tiles[ly as usize * 32 + lx as usize] = idx;
|
||||
}
|
||||
}
|
||||
chunk.version = 1;
|
||||
Ok(chunk)
|
||||
pub fn set_tile(&mut self, lx: u8, ly: u8, id: u16) {
|
||||
self.tiles[ly as usize * CHUNK_SIZE + lx as usize] = id;
|
||||
}
|
||||
|
||||
/// Set a tile at local coords. Bumps version if tile index or palette changed.
|
||||
pub fn set_tile(&mut self, lx: u8, ly: u8, def: TileDef) -> Result<(), PaletteFullError> {
|
||||
let (idx, was_new) = self.find_or_insert_raw(def)?;
|
||||
let slot = &mut self.tiles[ly as usize * 32 + lx as usize];
|
||||
let changed = *slot != idx;
|
||||
*slot = idx;
|
||||
if changed || was_new {
|
||||
self.version = self.version.wrapping_add(1);
|
||||
if self.version == 0 {
|
||||
self.version = 1;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_tile(&self, lx: u8, ly: u8) -> TileDef {
|
||||
self.palette[self.tiles[ly as usize * 32 + lx as usize] as usize]
|
||||
pub fn get_tile(&self, lx: u8, ly: u8) -> u16 {
|
||||
self.tiles[ly as usize * CHUNK_SIZE + lx as usize]
|
||||
}
|
||||
|
||||
pub fn tile_flags(&self, lx: u8, ly: u8) -> TileFlags {
|
||||
self.get_tile(lx, ly).flags
|
||||
crate::map::tile_flags(self.get_tile(lx, ly))
|
||||
}
|
||||
}
|
||||
|
||||
+5
-9
@@ -9,7 +9,7 @@ mod sim;
|
||||
|
||||
pub use sim::Sim;
|
||||
|
||||
use chunk::{Chunk, TileDef, TileFlags};
|
||||
use chunk::Chunk;
|
||||
use map::TileMap;
|
||||
use world::World;
|
||||
|
||||
@@ -99,8 +99,8 @@ pub fn tile_collidable(tile_id: u16) -> bool {
|
||||
|
||||
/// 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.
|
||||
/// get id 0 (solid, see `tile_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;
|
||||
@@ -109,12 +109,8 @@ pub fn load_world(map: &TileMap) -> World {
|
||||
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");
|
||||
map.tile(wx, wy) // 0 outside the map
|
||||
});
|
||||
w.set_chunk(cx, cy, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -64,9 +64,9 @@ impl TileMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gameplay flags for a global tile id. Collision comes from the shared movement
|
||||
/// vocabulary (`crate::tile_collidable`) so the client can predict it identically;
|
||||
/// other flags (opaque, …) stay sim-side. Tile ids index into `overworld.tga`.
|
||||
/// Gameplay flags for a global tile id. Collision comes from the movement vocabulary
|
||||
/// (`crate::tile_collidable`) so the pathfinder predicts it identically; other flags
|
||||
/// (opaque, …) will join here. Tile ids index into `overworld.png`.
|
||||
pub fn tile_flags(tile_id: u16) -> TileFlags {
|
||||
let mut bits = 0u8;
|
||||
if crate::tile_collidable(tile_id) {
|
||||
|
||||
+4
-6
@@ -69,16 +69,14 @@ impl Sim {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chunk::{Chunk, TileDef, TileFlags};
|
||||
use crate::chunk::Chunk;
|
||||
use crate::player_action;
|
||||
|
||||
/// Open floor (id 1) with a single wall tile at (5, 4) — id 146 is collidable in
|
||||
/// the tile vocabulary.
|
||||
fn open_world() -> World {
|
||||
let mut w = World::new();
|
||||
w.set_chunk(0, 0, Chunk::generate(|lx, ly| {
|
||||
// A single wall tile at (5, 4).
|
||||
let flags = if (lx, ly) == (5, 4) { TileFlags::COLLIDABLE } else { 0 };
|
||||
TileDef { tile_id: 1, flags: TileFlags(flags) }
|
||||
}).unwrap());
|
||||
w.set_chunk(0, 0, Chunk::generate(|lx, ly| if (lx, ly) == (5, 4) { 146 } else { 1 }));
|
||||
w
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::chunk::{Chunk, TileDef, TileFlags, PaletteFullError};
|
||||
use crate::chunk::{Chunk, TileFlags};
|
||||
use crate::entity::Entity;
|
||||
|
||||
pub fn tile_to_chunk(tx: i16, ty: i16) -> (i16, i16) {
|
||||
@@ -45,11 +45,11 @@ impl World {
|
||||
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> {
|
||||
pub fn set_tile(&mut self, wx: i16, wy: i16, id: u16) {
|
||||
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)
|
||||
chunk.set_tile(lx, ly, id);
|
||||
}
|
||||
|
||||
pub fn spawn_entity(&mut self, type_id: u16, pos: (i16, i16), hp: u16) -> u32 {
|
||||
|
||||
Reference in New Issue
Block a user