mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'src/data/map.rs')
-rw-r--r--src/data/map.rs696
1 files changed, 696 insertions, 0 deletions
diff --git a/src/data/map.rs b/src/data/map.rs
new file mode 100644
index 0000000..636e3f3
--- /dev/null
+++ b/src/data/map.rs
@@ -0,0 +1,696 @@
+//! the map module
+//! ### format
+//! note: utf = `len<u16>` + `utf8(read(len))`
+//!
+//! note: each section has a `u32` denoting its length
+//!
+//! key: `: T` and `x<T>` 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 `<u32>`
+//! - 1 byte of idk (skip)
+//! - string map (`u16` for map len, iterate each, read `utf`)
+//! - content header section `<u32>`:
+//! - 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 `<u32>`
+//! - 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 `<u32>`
+//! - entity mapping
+//! - iterate `u16`
+//! - id: `i16`, name: `utf`
+//! - team build plans
+//! - for t in `teams<u32>`
+//! - team = `team#<u32>`
+//! - iterate `plans<u32>`
+//! - 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<Build<'l>>,
+}
+
+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<u8, Box<dyn Content>>;
+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<Rotation> {
+ Some(self.build()?.rotation)
+ }
+}
+
+impl RotationState for Option<Tile<'_>> {
+ fn get_rotation(&self) -> Option<Rotation> {
+ self.as_ref().unwrap().get_rotation()
+ }
+}
+
+impl<'l> BlockState<'l> for Option<Tile<'_>> {
+ 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<Item>,
+ pub liquids: Storage<Fluid>,
+ pub state: Option<State>,
+ // 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<Item>) -> 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<Fluid>) -> 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<String, String>,
+ /// 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<Tile<'l>>,
+}
+
+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<String, String>) -> Self {
+ Self {
+ tiles: vec![Tile::new(BlockEnum::Air, BlockEnum::Air); width * height],
+ height,
+ width,
+ tags,
+ }
+ }
+}
+
+impl<'l> Index<usize> for Map<'l> {
+ type Output = Tile<'l>;
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.tiles[index]
+ }
+}
+
+impl<'l> IndexMut<usize> 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<Map<'l>> 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<Map<'l>, 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!()
+ }
+}