CSV-Map loading

This commit is contained in:
2026-06-17 10:28:32 +02:00
parent fd02ed6d8b
commit 5c8494961c
5 changed files with 211 additions and 15 deletions
+18 -9
View File
@@ -3,6 +3,7 @@ mod entity;
mod world;
mod sim;
mod net;
mod map;
use std::thread;
use std::time::{Duration, Instant};
@@ -11,22 +12,28 @@ use chunk::{Chunk, TileDef, TileFlags};
use world::World;
use sim::Sim;
use net::Net;
use map::TileMap;
const TICK_HZ: u32 = 24;
const TICK_DURATION: Duration = Duration::from_micros(1_000_000 / TICK_HZ as u64);
fn build_world() -> World {
/// 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();
for cy in -1i16..=1 {
for cx in -1i16..=1 {
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);
let border = wx == -32 || wx == 31 || wy == -32 || wy == 31;
if border {
TileDef { tile_id: 1, flags: TileFlags(TileFlags::COLLIDABLE | TileFlags::OPAQUE) }
} else {
TileDef::default()
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);
}
@@ -35,7 +42,9 @@ fn build_world() -> World {
}
fn main() {
let world = build_world();
let map = TileMap::from_csv("assets/map_test");
println!("loaded map {}×{} ({} tiles)", map.width, map.height, map.tiles.len());
let world = load_world(&map);
let mut sim = Sim::new(world);
let mut net = Net::new("127.0.0.1:7777").expect("failed to bind UDP socket");
+77
View File
@@ -0,0 +1,77 @@
//! Minimal Tiled-CSV map loader.
//!
//! Reads a bare comma-separated tile grid (Tiled's "CSV" layer export, stripped of the
//! surrounding TMX/XML). Dimensions are derived from the content: columns from the first
//! row, height from the row count. Tiled encodes per-tile flip/rotation in the top bits of
//! each 32-bit GID, which makes flipped tiles show up as large negative decimals — those
//! bits are masked off here, leaving the bare tile_id. Flip orientation is discarded for now.
use crate::chunk::TileFlags;
/// Top three bits hold horizontal / vertical / diagonal flip flags; the rest is the tile id.
const GID_FLIP_MASK: u32 = 0x1FFF_FFFF;
pub struct TileMap {
pub width: u16,
pub height: u16,
pub tiles: Vec<u16>, // row-major, len == width * height
}
impl TileMap {
pub fn from_csv(path: &str) -> Self {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("failed to read map {path}: {e}"));
let mut tiles = Vec::new();
let mut width = 0usize;
let mut height = 0usize;
for line in text.lines() {
if line.trim().is_empty() {
continue;
}
let mut cols = 0usize;
for field in line.split(',') {
let field = field.trim();
if field.is_empty() {
continue; // tolerate a trailing comma
}
// Parse as i64 so the "negative" flipped GIDs fit, then reinterpret the low
// 32 bits as u32 and strip the flip flags.
let raw = field.parse::<i64>()
.unwrap_or_else(|_| panic!("invalid tile value {field:?} in {path}"));
let gid = (raw as u32) & GID_FLIP_MASK;
tiles.push(gid as u16);
cols += 1;
}
if width == 0 {
width = cols;
} else if cols != width {
panic!("ragged map row in {path}: expected {width} cols, got {cols}");
}
height += 1;
}
TileMap { width: width as u16, height: height as u16, tiles }
}
/// Tile id at map coordinates, or 0 (empty) when outside the map bounds.
pub fn tile(&self, x: i16, y: i16) -> u16 {
if x < 0 || y < 0 || x >= self.width as i16 || y >= self.height as i16 {
return 0;
}
self.tiles[y as usize * self.width as usize + x as usize]
}
}
/// Gameplay flags for a global tile id. Hand-maintained vocabulary — extend as the
/// tileset grows. Unknown ids default to a plain walkable floor tile.
///
/// Tile ids correspond to indices into `overworld.tga` on the client side.
pub fn tile_flags(tile_id: u16) -> TileFlags {
match tile_id {
// TODO: fill in collidable / opaque tiles from overworld.tga, e.g.
// 146 => TileFlags(TileFlags::COLLIDABLE | TileFlags::OPAQUE),
_ => TileFlags::default(),
}
}