CSV-Map loading
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
|
||||
146,146,146,25,25,25,25,75,75,75,75,25,25,25,25,146,25,25,25,146,146,146,146,25,25,25,146,146,146,25
|
||||
25,146,146,25,75,75,75,75,75,75,146,146,146,146,146,25,25,25,25,25,25,25,25,25,25,25,25,146,146,25
|
||||
25,146,25,25,25,75,75,25,25,146,146,146,146,25,25,25,25,75,75,75,25,25,25,25,25,25,146,146,25,25
|
||||
25,146,146,25,25,25,75,75,75,75,146,146,146,146,25,25,25,75,75,75,75,75,25,25,25,25,146,25,25,25
|
||||
25,25,25,25,25,25,75,25,25,25,25,146,146,-2147483574,-2147483574,-2147483574,25,25,75,25,25,75,75,25,25,25,25,146,25,25
|
||||
25,25,25,25,25,25,75,25,25,25,25,-2147483574,-2147483574,146,25,-2147483574,-2147483574,25,75,25,25,25,75,25,25,25,25,146,25,25
|
||||
25,25,25,25,75,75,75,25,25,25,-2147483574,-2147483574,25,146,146,25,-2147483574,-2147483574,75,25,25,25,25,75,25,25,25,25,25,25
|
||||
25,25,25,75,75,75,25,25,25,-2147483574,-2147483574,25,25,25,146,146,25,-2147483574,-2147483574,75,25,25,25,75,75,25,25,25,25,25
|
||||
-2147483574,25,25,75,25,25,25,25,-2147483574,-2147483574,25,25,25,25,25,25,146,75,75,75,75,25,25,25,75,75,25,25,25,25
|
||||
25,-2147483574,-2147483574,75,25,25,25,25,-2147483574,25,25,25,25,25,25,25,25,146,75,75,75,75,25,25,25,75,25,25,25,25
|
||||
-2147483574,-2147483574,25,25,-2147483574,-2147483574,25,25,-2147483574,25,25,25,25,25,25,25,25,146,75,-2147483574,146,75,25,25,25,25,25,25,25,25
|
||||
25,25,-2147483574,25,25,25,-2147483574,-2147483574,-2147483574,-2147483574,25,25,25,25,25,75,75,75,146,75,146,146,146,25,25,25,25,25,25,25
|
||||
25,25,25,-2147483574,25,25,25,25,-2147483574,25,-2147483574,-2147483574,-2147483574,25,25,25,25,25,146,25,-2147483574,-2147483574,146,146,146,25,25,25,25,25
|
||||
25,25,25,25,-2147483574,-2147483574,25,25,-2147483574,-2147483574,-2147483574,25,25,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,25,146,-2147483574,-2147483574,-2147483574,25,25,146,25,25,25,25
|
||||
25,25,25,25,25,25,-2147483574,25,25,25,25,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,146,-2147483574,-2147483574,25,25,146,25,25,25,25
|
||||
25,25,25,25,25,25,25,-2147483574,25,25,25,25,25,25,25,25,-2147483574,-2147483574,25,25,146,-2147483574,-2147483574,25,146,146,25,25,25,25
|
||||
25,25,25,25,25,25,25,25,-2147483574,25,25,25,25,25,25,25,25,-2147483574,25,25,25,146,25,-2147483574,146,25,25,25,25,25
|
||||
25,25,25,25,25,25,25,25,-2147483574,25,25,25,25,25,25,25,25,-2147483574,25,25,25,25,146,146,146,-2147483574,-2147483574,25,25,25
|
||||
25,25,25,25,25,25,25,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,25,25,25,25,25,146,25,25,-2147483574,25,25,25
|
||||
25,25,25,25,75,25,25,25,25,25,-2147483574,-2147483574,25,25,25,25,25,25,25,25,25,25,25,146,25,25,25,25,25,25
|
||||
25,25,25,25,25,75,25,25,25,25,25,-2147483574,-2147483574,25,25,25,25,25,25,25,25,25,25,146,25,25,25,25,25,25
|
||||
25,25,25,25,25,25,75,25,25,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,-2147483574,25,25,25,25,25,25,25,25,25,146,146,25,25,25,25
|
||||
25,25,25,25,25,25,25,75,25,25,-2147483574,25,25,-2147483574,25,-2147483574,-2147483574,25,25,25,25,25,25,25,146,146,146,25,25,25
|
||||
25,25,146,25,25,25,25,25,75,25,25,-2147483574,-2147483574,25,-2147483574,-2147483574,-2147483574,25,25,25,25,25,25,146,146,146,146,25,25,25
|
||||
25,25,146,146,146,25,25,25,75,25,25,25,25,-2147483574,-2147483574,-2147483574,25,25,25,25,25,25,25,25,25,146,146,146,25,25
|
||||
25,25,146,146,146,146,25,25,25,75,25,75,75,75,75,75,75,75,75,25,25,25,25,25,25,25,25,146,146,25
|
||||
25,146,146,146,146,146,25,25,75,75,75,75,25,25,25,25,25,25,75,75,75,25,25,25,25,25,25,25,25,25
|
||||
25,25,146,25,25,25,25,75,75,75,75,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
|
||||
25,25,25,25,25,25,25,25,25,75,75,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25,25
|
||||
@@ -0,0 +1,77 @@
|
||||
# Map Loader
|
||||
|
||||
Loads level data from a Tiled CSV export, replacing the old hardcoded `build_world` wall loop.
|
||||
Implemented in `server/src/map.rs`; wired into the world in `server/src/main.rs::load_world`.
|
||||
|
||||
---
|
||||
|
||||
## Format
|
||||
|
||||
A bare comma-separated tile grid — Tiled's "CSV" layer export, stripped of the surrounding
|
||||
TMX/XML. No header, no dimensions. The loader derives them from the content:
|
||||
|
||||
- **width** = number of fields in the first row
|
||||
- **height** = number of rows
|
||||
|
||||
The current test map lives at `assets/map_test` (30×30, tile ids 25 / 74 / 75 / 146).
|
||||
A trailing comma per row is tolerated; ragged rows panic (they indicate a corrupt export).
|
||||
|
||||
---
|
||||
|
||||
## Tile flip flags — the "negative numbers"
|
||||
|
||||
Tiled packs per-tile flip/rotation into the **top 3 bits** of each 32-bit GID. A flipped tile
|
||||
therefore shows up in the CSV as a large *negative* decimal when read as `i32`. Example:
|
||||
|
||||
```
|
||||
-2147483574 as u32 = 0x8000004A
|
||||
│ └── 0x4A = 74 ← the real tile id
|
||||
└────────── 0x80000000 = horizontal flip
|
||||
```
|
||||
|
||||
| Bit | Mask | Meaning |
|
||||
|-----|--------------|---------------------|
|
||||
| 31 | `0x80000000` | horizontal flip |
|
||||
| 30 | `0x40000000` | vertical flip |
|
||||
| 29 | `0x20000000` | diagonal flip (rot) |
|
||||
|
||||
The loader parses each field as `i64`, reinterprets the low 32 bits as `u32`, and strips the
|
||||
flip flags with `& 0x1FFF_FFFF`, leaving the bare tile id. **Flip orientation is discarded for
|
||||
now** — flipped tiles render unflipped. Real flipping would need a flip-aware `blit_tile` on
|
||||
the client plus the flags carried through the protocol; deferred to the camera/sprite pass (09).
|
||||
|
||||
---
|
||||
|
||||
## Tile id ↔ tileset
|
||||
|
||||
Tile ids are used directly:
|
||||
|
||||
- **Server** — `tile_flags(id)` (in `map.rs`) maps an id to gameplay `TileFlags`
|
||||
(collidable / opaque). Hand-maintained vocabulary; currently everything is walkable floor.
|
||||
- **Client** — the id indexes straight into `overworld.tga` (`game.rs`). The tileset's tile
|
||||
order *is* the id space; keep the Tiled tileset and `overworld.tga` in lockstep.
|
||||
|
||||
No `firstgid` subtraction: the authored ids already line up with `overworld.tga`.
|
||||
|
||||
---
|
||||
|
||||
## World assembly
|
||||
|
||||
`load_world` slices the map into 32×32 chunks via `Chunk::generate`, which builds each chunk's
|
||||
6-bit palette automatically. The map's top-left tile sits at world (0, 0).
|
||||
|
||||
**Edge border:** a 30×30 map only fills part of chunk (0,0) (which spans tiles 0–31). Tiles
|
||||
inside a loaded chunk but outside the authored map are emitted as an *invisible solid border*
|
||||
(id 0, `COLLIDABLE`) so the walkable edge sits flush with the visible map rim rather than two
|
||||
tiles past it at the chunk boundary. Beyond any loaded chunk, movement is blocked anyway by
|
||||
`sim.rs`'s `map_or(true)` (no chunk = world edge).
|
||||
|
||||
---
|
||||
|
||||
## Not done yet
|
||||
|
||||
- Multiple layers (only a single tile layer is read)
|
||||
- Tile properties from Tiled (collision comes from the hand-maintained `tile_flags`, not the file)
|
||||
- Object layers (spawns, triggers)
|
||||
- Tile flipping (orientation discarded — see above)
|
||||
</content>
|
||||
+9
-6
@@ -28,6 +28,9 @@ terminal). The platform layer — window, GPU, input, RGB332 palette — lives i
|
||||
- [x] Ping/Pong RTT measurement (`PingPacket` / `PongPacket`)
|
||||
- [x] Connection timeout: client emits `Disconnected` after 10 s without a `StatePacket`;
|
||||
server evicts clients unseen for 10 s
|
||||
- [x] Map loader: Tiled-CSV parser in `server/src/map.rs` (flip-flag masking, content-derived
|
||||
dimensions), `load_world` slices it into chunks, invisible solid border at the map rim.
|
||||
Replaces the hardcoded `build_world`. See `notes/07-map-loader.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -48,14 +51,14 @@ This supersedes the original 20/10/1 Hz three-tier sketch.
|
||||
|
||||
---
|
||||
|
||||
### 07 — Tiled map loader
|
||||
### 07 — Tiled map loader *(done — see Achieved / `notes/07-map-loader.md`)*
|
||||
|
||||
Replace the hardcoded `build_world` wall loop with real level data.
|
||||
Remaining follow-ups when the need is concrete:
|
||||
|
||||
- Parse Tiled TMX (XML) or JSON export — only the subset actually used
|
||||
- Define the tile vocabulary: `TileKind` variants map 1:1 to Tiled tile IDs
|
||||
- Load a hand-authored starting area as the first real level
|
||||
- Procedural generation comes later; hand-authored first
|
||||
- Collision vocabulary: `tile_flags` in `map.rs` is currently empty (all walkable). Fill in
|
||||
collidable / opaque tile ids as the level design requires.
|
||||
- Tile flipping: orientation is discarded on load; revisit with the sprite pass (09).
|
||||
- Multiple / object layers (spawns, triggers) — not yet parsed.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+18
-9
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user