//! the map module //! ### format //! note: utf = `len` + `utf8(read(len))` //! //! note: each section has a `u32` denoting its length //! //! key: `: T` and `x` both mean read T, `iterate T` means iterate `read_T()` times //! //! ZLIB compressed stream contains: //! - header: 4b = `MSCH` //! - version: `u32` (should be 7) //! - tag section `` //! - 1 byte of idk (skip) //! - string map (`u16` for map len, iterate each, read `utf`) //! - content header section ``: //! - iterate `i8` (should = `8`)' //! - the type: `i8` (0: item, block: 1, liquid: 4, status: 5, unit: 6, weather: 7, sector: 9, planet: 13//! - item count: `u16` (item: 22, block: 412, liquid: 11, status: 21, unit: 66, weather: 6, sector: 35, planet: 7) //! - these types all have their own modules: [`item`], [`content`], [`fluid`], [`modifier`], [`mod@unit`], [`weather`], [`sector`], [`planet`] //! - iterate `u16` //! - name: `utf` //! - map section `` //! - width: `u16`, height: `u16` //! - floor and tiles: //! - for `i` in `w * h` //! - `x = i % w`, `y = i / w` //! - floor id: `u16` //! - overlay id: `u16` //! - consecutives: `u8` //! - iterate `(i + 1)..(i + 1 + consecutives)` //! - `x = j % w`, `y = j / w` //! - i += consecutives //! - blocks //! - for `i` in `w * h` //! - block id: `u16` //! - packed?: `i8` //! - entity = `(packed & 1) not 0` //! - data = `(packed & 2) not 0` //! - if entity: central: `bool` //! - if entity: //! - if central: //! - chunk len: `u16` //! - if block == building: //! - revision: `i8` //! - [`read`] //! - else skip `chunk len` //! - or data //! - data: `i8` //! - else //! - consecutives: `u8` //! - iterate `(i + 1)..(i + 1 + consecutives)` //! - same block //! - i += consecutives //! - entities section `` //! - entity mapping //! - iterate `u16` //! - id: `i16`, name: `utf` //! - team build plans //! - for t in `teams` //! - team = `team#` //! - iterate `plans` //! - x: `u16`, y: `u16`, rot: `u16`, id: `u16` //! - o: `DynData` (refer to [`DynSerializer`]) //! - world entities //! - iterate `u32` //! - len: `u16` //! - type: `u8` //! - if !mapping\[type\] //! - skip(len - 1) //! - continue //! - id: `u32` //! - entity read use std::collections::HashMap; use std::ops::{Index, IndexMut}; use thiserror::Error; use crate::block::content::Type as BlockEnum; use crate::block::{Block, BlockRegistry, Rotation, State}; use crate::data::dynamic::DynSerializer; use crate::data::renderer::*; use crate::data::DataRead; use crate::fluid::Type as Fluid; use crate::item::{storage::Storage, Type as Item}; use crate::team::{self, Team}; #[cfg(doc)] use crate::{block::content, data::*, fluid, item, modifier, unit}; use super::Serializer; use crate::content::Content; use crate::utils::image::ImageUtils; /// a tile in a map #[derive(Clone)] pub struct Tile<'l> { pub floor: BlockEnum, pub ore: BlockEnum, build: Option>, } macro_rules! lo { ($v:expr => [$(|)? $($k:literal $(|)?)+], $scale: ident) => { paste::paste! { match $v { $(BlockEnum::[<$k:camel>] => load!($k, $scale),)+ n => unreachable!("{n:?}"), } } }; } pub type EntityMapping = HashMap>; impl<'l> Tile<'l> { #[must_use] pub const fn new(floor: BlockEnum, ore: BlockEnum) -> Self { Self { floor, ore, build: None, } } fn set_block(&mut self, block: &'l Block) { self.build = Some(Build { block, state: None, items: Storage::new(), liquids: Storage::new(), rotation: Rotation::Up, team: crate::team::SHARDED, data: 0, }); } #[must_use] pub const fn build(&self) -> Option<&Build<'l>> { self.build.as_ref() } /// size of this tile /// /// ._. /// /// dont think about it too much #[must_use] #[inline] pub fn size(&self) -> u8 { self.build.as_ref().map_or(1, |v| v.block.get_size()) } #[inline] pub(crate) fn floor(&self, s: Scale) -> ImageHolder<4> { lo!(self.floor => [ | "darksand" | "sand-floor" | "dacite" | "dirt" | "arkycite-floor" | "basalt" | "moss" | "mud" | "grass" | "ice-snow" | "snow" | "salt" | "ice" | "hotrock" | "char" | "magmarock" | "shale" | "metal-floor" | "metal-floor-2" | "metal-floor-3" | "metal-floor-4" | "metal-floor-5" | "metal-floor-damaged" | "dark-panel-1" | "dark-panel-2" | "dark-panel-3" | "dark-panel-4" | "dark-panel-5" | "dark-panel-6" | "darksand-tainted-water" | "darksand-water" | "deep-tainted-water" | "deep-water" | "sand-water" | "shallow-water" | "tainted-water" | "tar" | "pooled-cryofluid" | "molten-slag" | "space" | "stone" | "bluemat" | "ferric-craters" | "beryllic-stone" | "rhyolite" | "rough-rhyolite" | "rhyolite-crater" | "rhyolite-vent" | "core-zone" | "crater-stone" | "redmat" | "red-ice" | "spore-moss" | "regolith" | "ferric-stone" | "arkyic-stone" | "arkyic-vent" | "yellow-stone" | "yellow-stone-plates" | "yellow-stone-vent" | "red-stone" | "red-stone-vent" | "dense-red-stone" | "carbon-stone" | "carbon-vent" | "crystal-floor" | "crystalline-stone" | "crystalline-vent" | "empty"], s) } #[must_use] #[inline] pub(crate) fn ore(&self, s: Scale) -> ImageHolder<4> { lo!(self.ore => ["ore-copper" | "ore-beryllium" | "ore-lead" | "ore-scrap" | "ore-coal" | "ore-thorium" | "ore-titanium" | "ore-tungsten" | "pebbles" | "tendrils" | "ore-wall-tungsten" | "ore-wall-beryllium" | "ore-wall-thorium" | "spawn" | "ore-crystal-thorium"], s) } #[must_use] #[inline] pub fn has_ore(&self) -> bool { self.ore != BlockEnum::Air } /// Draw the floor of this tile #[must_use] pub fn floor_image(&self, s: Scale) -> ImageHolder<4> { let mut floor = self.floor(s); if self.has_ore() { unsafe { floor.overlay(&self.ore(s)) }; } floor } /// Draw this tiles build. #[must_use] #[inline] pub fn build_image(&self, context: Option<&RenderingContext>, s: Scale) -> ImageHolder<4> { // building covers floore let Some(b) = &self.build else { unreachable!(); }; b.image(context, s) } } impl std::fmt::Debug for Tile<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Tile@{}{}{}", self.floor.get_name(), if self.ore != BlockEnum::Air { format!("+{}", self.ore.get_name()) } else { String::new() }, if let Some(build) = &self.build { format!(":{}", build.block.name()) } else { String::new() } ) } } impl<'l> BlockState<'l> for Tile<'l> { fn get_block(&self) -> Option<&'l Block> { Some(self.build()?.block) } } impl RotationState for Tile<'_> { fn get_rotation(&self) -> Option { Some(self.build()?.rotation) } } impl RotationState for Option> { fn get_rotation(&self) -> Option { self.as_ref().unwrap().get_rotation() } } impl<'l> BlockState<'l> for Option> { fn get_block(&'l self) -> Option<&'l Block> { self.as_ref().unwrap().get_block() } } /// a build on a tile in a map #[derive(Clone)] pub struct Build<'l> { pub block: &'l Block, pub items: Storage, pub liquids: Storage, pub state: Option, // pub health: f32, pub rotation: Rotation, pub team: Team, pub data: i8, } impl std::fmt::Debug for Build<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Build<{block}>", block = self.block.name(),) } } impl<'l> Build<'l> { #[must_use] pub fn new(block: &'l Block) -> Build<'l> { Self { block, items: Storage::default(), liquids: Storage::default(), state: None, rotation: Rotation::Up, team: team::SHARDED, data: 0, } } fn image(&self, context: Option<&RenderingContext>, s: Scale) -> ImageHolder<4> { self.block .image(self.state.as_ref(), context, self.rotation, s) } #[must_use] pub const fn name(&self) -> &str { self.block.name() } pub fn read( &mut self, buff: &mut DataRead<'_>, reg: &BlockRegistry, map: &EntityMapping, ) -> Result<(), ReadError> { // health let _ = buff.read_f32()?; let rot = buff.read_i8()? as i16; // team let _ = buff.read_i8()?; self.rotation = Rotation::try_from((rot & 127) as u8).unwrap_or(Rotation::Up); let mut mask = 0; let mut version = 0; if rot & 128 != 0 { version = buff.read_u8()?; if version < 3 { return Err(ReadError::Version(version)); } buff.skip(1)?; mask = buff.read_u8()?; } if mask & 1 != 0 { read_items(buff, &mut self.items)?; } if mask & 2 != 0 { read_power(buff)?; } if mask & 4 != 0 { read_liquids(buff, &mut self.liquids)?; } // "efficiency"? buff.skip(2)?; if version == 4 { // visible flags for fog buff.skip(4)?; } // "overridden by subclasses" self.block.read(self, reg, map, buff)?; // implementation not complete, simply error, causing the remaining bytes in the chunk to be skipped (TODO finish impl) Err(ReadError::Version(0x0)) // Ok(()) } } /// format: /// - iterate [`u16`] /// - item: [`u16`] as [`Item`] /// - amount: [`u32`] /// fn read_items(from: &mut DataRead, to: &mut Storage) -> Result<(), ReadError> { to.clear(); for _ in 0..from.read_u16()? { let item = from.read_u16()?; let amount = from.read_u32()?; if let Ok(item) = Item::try_from(item) { to.set(item, amount); } } Ok(()) } /// format: /// - iterate [`u16`] /// - liquid: [`u16`] as [`Fluid`] /// - amount: [`f32`] fn read_liquids(from: &mut DataRead, to: &mut Storage) -> Result<(), ReadError> { to.clear(); for _ in 0..from.read_u16()? { let fluid = from.read_u16()?; let amount = from.read_f32()?; if let Ok(fluid) = Fluid::try_from(fluid) { to.set(fluid, (amount * 100.0) as u32); } } Ok(()) } /// format: /// - iterate [`u16`] /// - link: [`i32`] /// - status: [`f32`] fn read_power(from: &mut DataRead) -> Result<(), ReadError> { let n = from.read_u16()? as usize; from.skip((n + 1) * 4)?; Ok(()) } #[test] fn test_read_items() { let mut s = Storage::new(); read_items( &mut DataRead::new(&[ 0, 6, 0, 0, 0, 0, 2, 187, 0, 1, 0, 0, 1, 154, 0, 2, 0, 0, 15, 160, 0, 3, 0, 0, 0, 235, 0, 6, 0, 0, 1, 46, 0, 12, 0, 0, 1, 81, 255, 255, ]), &mut s, ) .unwrap(); assert!(s.get_total() == 5983); } #[test] fn test_read_liquids() { let mut s = Storage::new(); read_liquids( &mut DataRead::new(&[0, 1, 0, 0, 67, 111, 247, 126, 255, 255]), &mut s, ) .unwrap(); assert!(s.get(Fluid::Water) == 23996); } /// a map. /// ## Does not support serialization yet! #[derive(Debug)] pub struct Map<'l> { pub width: usize, pub height: usize, pub tags: HashMap, /// row major 2d array /// ```rs /// (0, 0), (1, 0), (2, 0) /// (0, 1), (1, 1), (2, 1) /// (0, 2), (1, 2), (2, 2) /// ``` pub tiles: Vec>, } macro_rules! cond { ($cond: expr, $do: expr) => { if $cond { None } else { $do } }; } impl<'l> Crossable for Map<'l> { // N // cond!(pos.position.1 >= (pos.height - 1) as u16, get(j + 1)), // // E // cond!( // pos.position.0 >= (pos.height - 1) as u16, // get(j + pos.height) // ), // // S // cond!( // pos.position.1 == 0 || pos.position.1 >= pos.height as u16, // cond!(pos.position.1 >= (pos.height - 1), get(j + 1)), // // E // cond!(pos.position.0 >= (pos.height - 1), get(j + pos.height)), // // S // cond!( // pos.position.1 == 0 || pos.position.1 >= pos.height, // get(j - 1) // ), // // W // cond!(j < pos.height, get(j - pos.height)), fn cross(&self, j: usize, c: &PositionContext) -> Cross { let get = |i| { let b = &self[i]; Some((b.get_block()?, b.get_rotation()?)) }; [ cond![ c.position.1 == 0 || c.position.1 >= c.height, get(j + self.height) ], cond![c.position.0 >= (c.height - 1), get(j + 1)], cond![c.position.1 >= (c.height - 1), get(j - self.width)], cond![j < c.height, get(j - 1)], ] } } impl<'l> Map<'l> { #[must_use] pub fn new(width: usize, height: usize, tags: HashMap) -> Self { Self { tiles: vec![Tile::new(BlockEnum::Air, BlockEnum::Air); width * height], height, width, tags, } } } impl<'l> Index for Map<'l> { type Output = Tile<'l>; fn index(&self, index: usize) -> &Self::Output { &self.tiles[index] } } impl<'l> IndexMut for Map<'l> { fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.tiles[index] } } const MAP_HEADER: [u8; 4] = [b'M', b'S', b'A', b'V']; /// error occurring when reading a map fails #[derive(Debug, Error)] pub enum ReadError { #[error("failed to read from buffer")] Read(#[from] super::ReadError), #[error(transparent)] Decompress(#[from] super::DecompressError), #[error("incorrect header ({0:?})")] Header([u8; 4]), #[error("unsupported version ({0})")] Version(u8), #[error("unknown block {0:?}")] NoSuchBlock(String), #[error("failed to read block data")] ReadState(#[from] super::dynamic::ReadError), } /// serde map pub struct MapSerializer<'l>(pub &'l BlockRegistry<'l>); impl<'l> Serializer> for MapSerializer<'l> { type ReadError = ReadError; type WriteError = (); /// deserialize a map /// /// notes: /// - does not deserialize data /// - does not deserialize entities fn deserialize(&mut self, buff: &mut DataRead<'_>) -> Result, Self::ReadError> { let buff = buff.deflate()?; let mut buff = DataRead::new(&buff); { let mut b = [0; 4]; buff.read_bytes(&mut b)?; if b != MAP_HEADER { return Err(ReadError::Header(b)); } } let version = buff.read_u32()?; if version != 7 { return Err(ReadError::Version(version.try_into().unwrap_or(0))); } let mut tags = HashMap::new(); buff.read_chunk(true, |buff| { buff.skip(1)?; for _ in 0..buff.read_u8()? { let key = buff.read_utf()?; let value = buff.read_utf()?; tags.insert(key.to_owned(), value.to_owned()); } Ok::<(), super::ReadError>(()) })?; // we skip the content header (just keep the respective modules updated) buff.skip_chunk()?; // map section let mut w = 0; let mut h = 0; let mut m = None; buff.read_chunk(true, |buff| { w = buff.read_u16()? as usize; h = buff.read_u16()? as usize; let mut map = Map::new(w, h, tags); let count = w * h; let mut i = 0; while i < count { let floor_id = buff.read_u16()?; let overlay_id = buff.read_u16()?; let floor = BlockEnum::try_from(floor_id).unwrap_or(BlockEnum::Stone); let ore = BlockEnum::try_from(overlay_id).unwrap_or(BlockEnum::Air); map[i] = Tile::new(floor, ore); let consecutives = buff.read_u8()? as usize; if consecutives > 0 { for i in (i + 1)..(i + 1 + consecutives) { map[i] = Tile::new(floor, ore); } i += consecutives; } i += 1; } let mut i = 0usize; while i < count { let block_id = buff.read_u16()?; let packed = buff.read_u8()?; let entity = (packed & 1) != 0; let data = (packed & 2) != 0; let central = if entity { buff.read_bool()? } else { false }; let block = BlockEnum::try_from(block_id) .map_err(|_| ReadError::NoSuchBlock(block_id.to_string()))?; let block = if block == BlockEnum::Air { None } else { Some( self.0 .get(block.get_name()) .ok_or_else(|| ReadError::NoSuchBlock(block.to_string()))?, ) }; if central { if let Some(block) = block { map[i].set_block(block); } } if entity { if central { let mut output = [0u8; 2]; output.copy_from_slice(&buff.data[..2]); let _ = buff.read_chunk(false, |buff| { #[cfg(debug_assertions)] println!("reading {:?}", map[i].build.as_ref().unwrap()); let _ = buff.read_i8()?; map[i] .build .as_mut() .unwrap() // map not initialized yet .read(buff, self.0, &HashMap::new())?; Ok::<(), ReadError>(()) }); } } else if data { if let Some(block) = block { map[i].set_block(block); } map[i].build.as_mut().unwrap().data = buff.read_i8()?; } else { let consecutives = buff.read_u8()? as usize; for i in i..=i + consecutives { if let Some(block) = block { map.tiles[i].set_block(block); } } i += consecutives; } i += 1; } m = Some(map); Ok::<(), ReadError>(()) })?; let mut mapping = EntityMapping::new(); buff.read_chunk(true, |buff| { // read entity mapping (SaveVersion.java#436) for _ in 0..buff.read_u16()? { let id = buff.read_u16()? as u8; let nam = buff.read_utf()?; dbg!(nam); mapping.insert(id, Box::new(Item::Copper)); // mapping.push(content::Type::get_name(nam)); } // read team block plans (ghosts) (SaveVersion.java#389) for _ in 0..buff.read_u32()? { buff.skip(4)?; for _ in 0..buff.read_u32()? { buff.skip(8usize)?; let _ = DynSerializer::deserialize(&mut DynSerializer, buff)?; } } // read world entities (#412). eg units for _ in 0..buff.read_u32()? { let len = buff.read_u16()? as usize; let ty = buff.read_u8()?; if !mapping.contains_key(&ty) { buff.skip(len - 1)?; continue; } let _id = buff.read_u32()?; // TODO } Ok::<(), ReadError>(()) })?; // skip custom chunks buff.skip_chunk()?; Ok(m.unwrap()) } /// serialize a map (todo) /// panics: always fn serialize( &mut self, _: &mut super::DataWrite<'_>, _: &Map<'_>, ) -> Result<(), Self::WriteError> { todo!() } }