From dcf8de18eb2a5f558ed6f21013113ba1e9d3a3c4 Mon Sep 17 00:00:00 2001 From: random-geek <35757396+random-geek@users.noreply.github.com> Date: Tue, 18 May 2021 00:11:01 -0700 Subject: [PATCH] Completion Part 6 The Final Frontier? --- src/main.rs | 4 +- src/map_block/map_block.rs | 191 +++++++++++++++++- src/map_block/metadata.rs | 83 +++++++- src/map_block/mod.rs | 107 ++++++++-- src/map_block/name_id_map.rs | 2 +- src/map_block/node_data.rs | 2 +- src/map_block/node_timer.rs | 2 +- src/map_block/static_object.rs | 46 ++++- src/{time_keeper.rs => testing.rs} | 28 +++ test_data/mapblock_v25.bin | Bin 0 -> 347 bytes test_data/mapblock_v28.bin | Bin 0 -> 400 bytes test_data/test_mod/init.lua | 91 +++++++++ test_data/test_mod/textures/test_mod_test.png | Bin 0 -> 144 bytes 13 files changed, 523 insertions(+), 33 deletions(-) rename src/{time_keeper.rs => testing.rs} (68%) create mode 100644 test_data/mapblock_v25.bin create mode 100644 test_data/mapblock_v28.bin create mode 100644 test_data/test_mod/init.lua create mode 100644 test_data/test_mod/textures/test_mod_test.png diff --git a/src/main.rs b/src/main.rs index 1033151..3f11f1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -// Kept for testing purposes -// mod time_keeper; +// Uncomment if needed for testing +// mod testing; mod spatial; mod utils; mod map_database; diff --git a/src/map_block/map_block.rs b/src/map_block/map_block.rs index a970c5a..662173f 100644 --- a/src/map_block/map_block.rs +++ b/src/map_block/map_block.rs @@ -1,5 +1,13 @@ use super::*; +/* +Supported mapblock versions: +25: In use from 0.4.2-rc1 until 0.4.15. +26: Only ever sent over the network, not saved. +27: Existed for around 3 months during 0.4.16 development. +28: In use since 0.4.16. +*/ + const MIN_BLOCK_VER: u8 = 25; const MAX_BLOCK_VER: u8 = 28; const BLOCK_BUF_SIZE: usize = 2048; @@ -34,7 +42,7 @@ impl MapBlock { // Version let version = data.read_u8()?; if version < MIN_BLOCK_VER || version > MAX_BLOCK_VER { - return Err(MapBlockError::InvalidVersion); + return Err(MapBlockError::InvalidBlockVersion); } // Flags @@ -43,13 +51,14 @@ impl MapBlock { // Light data let lighting_complete = if version >= 27 { data.read_u16::()? } - else { 0 }; + else { 0xFFFF }; // Content width/param width let content_width = data.read_u8()?; let params_width = data.read_u8()?; + // TODO: support content_width == 1? if content_width != 2 || params_width != 2 { - return Err(MapBlockError::Other); + return Err(MapBlockError::InvalidFeature); } // Node data @@ -121,3 +130,179 @@ impl MapBlock { buf } } + + +#[cfg(test)] +mod tests { + use super::*; + use crate::spatial::Vec3; + // use crate::testing::debug_bytes; // TODO + use std::path::Path; + + fn read_test_file(filename: &str) -> anyhow::Result> { + let cargo_path = std::env::var("CARGO_MANIFEST_DIR")?; + let path = Path::new(&cargo_path).join("test_data").join(filename); + Ok(std::fs::read(path)?) + } + + #[test] + fn test_mapblock_v28() { + // Original block positioned at (0, 0, 0). + let original_data = read_test_file("mapblock_v28.bin").unwrap(); + let block = MapBlock::deserialize(&original_data).unwrap(); + + /* Ensure that all block data is correct. */ + assert_eq!(block.version, 28); + assert_eq!(block.flags, 0x03); + assert_eq!(block.lighting_complete, 0xF1C4); + assert_eq!(block.content_width, 2); + assert_eq!(block.params_width, 2); + + // Probe a few spots in the node data. + let nd = block.node_data.get_ref(); + let test_node_id = block.nimap.get_id(b"test_mod:timer").unwrap(); + let air_id = block.nimap.get_id(b"air").unwrap(); + assert_eq!(nd.nodes[0x000], test_node_id); + assert!(nd.nodes[0x001..=0xFFE].iter().all(|&n| n == air_id)); + assert_eq!(nd.nodes[0xFFF], test_node_id); + assert_eq!(nd.param1[0x111], 0x0F); + assert_eq!(nd.param2[0x000], 4); + assert!(nd.param2[0x001..=0xFFE].iter().all(|&n| n == 0)); + assert_eq!(nd.param2[0xFFF], 16); + + assert_eq!(block.metadata.get_ref(), b"\x00"); + + let obj1 = &block.static_objects[0]; + assert_eq!(obj1.obj_type, 7); + assert_eq!(obj1.f_pos, Vec3::new(8, 9, 12) * 10_000); + assert_eq!(obj1.data.len(), 62); + let obj2 = &block.static_objects[1]; + assert_eq!(obj2.obj_type, 7); + assert_eq!(obj2.f_pos, Vec3::new(1, 2, 2) * 10_000); + assert_eq!(obj2.data.len(), 81); + + assert_eq!(block.timestamp, 2756); + + assert_eq!(block.nimap.0[&0], b"test_mod:timer"); + assert_eq!(block.nimap.0[&1], b"air"); + + assert_eq!(block.node_timers[0].pos, 0xFFF); + assert_eq!(block.node_timers[0].timeout, 1337); + assert_eq!(block.node_timers[0].elapsed, 600); + assert_eq!(block.node_timers[1].pos, 0x000); + assert_eq!(block.node_timers[1].timeout, 1337); + assert_eq!(block.node_timers[1].elapsed, 200); + + /* Test re-serialized data */ + let new_data = block.serialize(); + + // If zlib-compressed data is reused, it should be identical. + assert_eq!(new_data, original_data); + + // Triggering a data modification should change the compressed data, + // since Minetest and MapEditr use different compression levels. + let mut block2 = block.clone(); + block2.node_data.get_mut(); + assert_ne!(block2.serialize(), original_data); + } + + #[test] + fn test_mapblock_v25() { + // Original block positioned at (-1, -1, -1). + let original_data = read_test_file("mapblock_v25.bin").unwrap(); + let block = MapBlock::deserialize(&original_data).unwrap(); + + /* Ensure that all block data is correct. */ + assert_eq!(block.version, 25); + assert_eq!(block.flags, 0x03); + assert_eq!(block.lighting_complete, 0xFFFF); + assert_eq!(block.content_width, 2); + assert_eq!(block.params_width, 2); + + let nd = block.node_data.get_ref(); + let test_node_id = block.nimap.get_id(b"test_mod:stone").unwrap(); + for z in &[0, 15] { + for y in &[0, 15] { + for x in &[0, 15] { + assert_eq!(nd.nodes[x + 16 * (y + 16 * z)], test_node_id); + } + } + } + assert_eq!(nd.nodes[0x001], block.nimap.get_id(b"air").unwrap()); + assert_eq!(nd.nodes[0x111], + block.nimap.get_id(b"test_mod:timer").unwrap()); + assert_eq!(nd.param2[0x111], 12); + + assert_eq!(block.metadata.get_ref(), b"\x00"); + + let obj1 = &block.static_objects[0]; + assert_eq!(obj1.obj_type, 7); + assert_eq!(obj1.f_pos, Vec3::new(-5, -10, -15) * 10_000); + assert_eq!(obj1.data.len(), 72); + + let obj2 = &block.static_objects[1]; + assert_eq!(obj2.obj_type, 7); + assert_eq!(obj2.f_pos, Vec3::new(-14, -12, -10) * 10_000); + assert_eq!(obj2.data.len(), 54); + + assert_eq!(block.timestamp, 2529); + + assert_eq!(block.nimap.0[&0], b"test_mod:stone"); + assert_eq!(block.nimap.0[&1], b"air"); + assert_eq!(block.nimap.0[&2], b"test_mod:timer"); + + assert_eq!(block.node_timers[0].pos, 0x111); + assert_eq!(block.node_timers[0].timeout, 1337); + assert_eq!(block.node_timers[0].elapsed, 0); + + /* Test re-serialized data */ + let mut block2 = block.clone(); + block2.node_data.get_mut(); + assert_ne!(block2.serialize(), original_data); + } + + #[test] + fn test_failures() { + let data = read_test_file("mapblock_v28.bin").unwrap(); + + // Change specific parts of the serialized data and make sure + // MapBlock::deserialize() catches the errors. Something like a hex + // editor is needed to follow along. + + let check_error = + |modder: fn(&mut [u8]), expected_error: MapBlockError| + { + let mut copy = data.clone(); + modder(&mut copy); + assert_eq!(MapBlock::deserialize(©).unwrap_err(), + expected_error); + }; + + // Invalid versions + check_error(|d| d[0x0] = 24, MapBlockError::InvalidBlockVersion); + check_error(|d| d[0x0] = 29, MapBlockError::InvalidBlockVersion); + // Invalid content width + check_error(|d| d[0x4] = 1, MapBlockError::InvalidFeature); + // Invalid parameter width + check_error(|d| d[0x5] = 3, MapBlockError::InvalidFeature); + // Invalid static object version + check_error(|d| d[0xA9] = 1, MapBlockError::InvalidSubVersion); + // Invalid name-ID map version + check_error(|d| d[0x15D] = 1, MapBlockError::InvalidSubVersion); + // Invalid node timer data length + check_error(|d| d[0x179] = 12, MapBlockError::InvalidFeature); + + { // Invalid node data size + let mut block = MapBlock::deserialize(&data).unwrap(); + block.node_data.get_mut().param1.push(0); + let new_data = block.serialize(); + assert_eq!(MapBlock::deserialize(&new_data).unwrap_err(), + MapBlockError::BadData); + + block.node_data.get_mut().param1.truncate(4095); + let new_data = block.serialize(); + assert_eq!(MapBlock::deserialize(&new_data).unwrap_err(), + MapBlockError::BadData); + } + } +} diff --git a/src/map_block/metadata.rs b/src/map_block/metadata.rs index de5aca2..5242980 100644 --- a/src/map_block/metadata.rs +++ b/src/map_block/metadata.rs @@ -33,9 +33,11 @@ impl NodeMetadata { } let end_finder = TwoWaySearcher::new(END_STR); + // This should be safe; EndInventory\n cannot appear in item metadata + // since newlines are escaped. let end = end_finder .search_in(&data.get_ref()[data.position() as usize ..]) - .ok_or(MapBlockError::Other)?; + .ok_or(MapBlockError::BadData)?; let mut inv = vec_with_len(end + END_STR.len()); data.read_exact(&mut inv)?; @@ -79,7 +81,7 @@ impl NodeMetadataListExt for NodeMetadataList { let version = data.read_u8()?; if version > 2 { - return Err(MapBlockError::InvalidVersion) + return Err(MapBlockError::InvalidSubVersion) } let count = match version { @@ -121,3 +123,80 @@ impl NodeMetadataListExt for NodeMetadataList { data.into_inner() } } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_meta_serialize() { + // Test empty metadata lists + assert!(NodeMetadataList::deserialize(b"\x00").unwrap().is_empty()); + for &ver in &[25, 28] { + assert_eq!(NodeMetadataList::new().serialize(ver), b"\x00"); + } + + // Test serialization/deserialization and filtering of empty metadata. + let meta_in = b"\x02\x00\x04\ + \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ + list[context;main;0,0;4,1;]\x00List main 4\nWidth 0\nEmpty\n\ + Empty\nItem basenodes:cobble 1 0 \"\\u0001check\\u0002\ + EndInventory\\n\\u0003\"\nEmpty\nEndInventoryList\n\ + EndInventory\n\ + \x0e\x21\x00\x00\x00\x01\x00\x06secret\x00\x00\x00\x0a\x01pa55w0rd\ + \x02\x01EndInventory\n\ + \x03\x23\x00\x00\x00\x00EndInventory\n\ + \x0f\xff\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ + with_grass 10\nEndInventoryList\nEndInventory\n"; + + let meta_out = b"\x02\x00\x03\ + \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ + list[context;main;0,0;4,1;]\x00List main 4\nWidth 0\nEmpty\n\ + Empty\nItem basenodes:cobble 1 0 \"\\u0001check\\u0002\ + EndInventory\\n\\u0003\"\nEmpty\nEndInventoryList\n\ + EndInventory\n\ + \x0e\x21\x00\x00\x00\x01\x00\x06secret\x00\x00\x00\x0a\x01pa55w0rd\ + \x02\x01EndInventory\n\ + \x0f\xff\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ + with_grass 10\nEndInventoryList\nEndInventory\n"; + + let meta_list = NodeMetadataList::deserialize(&meta_in[..]).unwrap(); + assert_eq!(meta_list.len(), 4); + assert_eq!(meta_list[&0x010].vars[&b"formspec"[..]].1, false); + assert_eq!(meta_list[&0xe21].vars[&b"secret"[..]].1, true); + assert_eq!(meta_list.serialize(28), meta_out); + + // Test currently unsupported version + let mut meta_future = meta_in.to_vec(); + meta_future[0] = b'\x03'; + assert_eq!( + NodeMetadataList::deserialize(&meta_future[..]).unwrap_err(), + MapBlockError::InvalidSubVersion + ); + + // Test old version + let meta_v1 = b"\x01\x00\x02\ + \x00\x10\x00\x00\x00\x01\x00\x08formspec\x00\x00\x00\x24size[4,1]\ + list[context;main;0,0;4,1;]List main 4\nWidth 0\nEmpty\n\ + Empty\nItem basenodes:cobble\nEmpty\nEndInventoryList\n\ + EndInventory\n\ + \x0d\xb7\x00\x00\x00\x00List main 1\nWidth 0\nItem basenodes:dirt_\ + with_grass 10\nEndInventoryList\nEndInventory\n"; + + let meta_list_v1 = + NodeMetadataList::deserialize(&meta_v1[..]).unwrap(); + assert_eq!(meta_list_v1.len(), 2); + assert_eq!(meta_list_v1[&0x010].vars[&b"formspec"[..]].1, false); + assert_eq!(meta_list_v1.serialize(25), meta_v1); + + // Test missing inventory + let missing_inv = b"\x02\x00\x02\ + \x01\x23\x00\x00\x00\x01\ + \x00\x03foo\x00\x00\x00\x03bar\x00 + \x0f\xed\x00\x00\x00\x01\ + \x00\x0dfake_inv_test\x00\x00\x00\x0cEndInventory\x00"; + assert_eq!(NodeMetadataList::deserialize(missing_inv).unwrap_err(), + MapBlockError::BadData); + } +} diff --git a/src/map_block/mod.rs b/src/map_block/mod.rs index f2773bb..8accb67 100644 --- a/src/map_block/mod.rs +++ b/src/map_block/mod.rs @@ -1,5 +1,6 @@ use std::io::prelude::*; use std::io::Cursor; +use std::convert::TryFrom; use byteorder::{ByteOrder, BigEndian, ReadBytesExt, WriteBytesExt}; @@ -23,16 +24,21 @@ use node_timer::{serialize_timers, deserialize_timers}; pub use name_id_map::NameIdMap; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum MapBlockError { - InvalidVersion, - DataError, - Other, + /// Block data is malformed or missing. + BadData, + /// The block version is unsupported. + InvalidBlockVersion, + /// Some data length or other value is unsupported. + InvalidFeature, + /// Some content within the mapblock has an unsupported version. + InvalidSubVersion, } impl From for MapBlockError { fn from(_: std::io::Error) -> Self { - Self::DataError + Self::BadData } } @@ -48,11 +54,11 @@ fn vec_with_len(len: usize) -> Vec { /// enough bytes in `src`. #[inline(always)] fn try_read_n(src: &mut Cursor<&[u8]>, n: usize) - -> Result, std::io::Error> + -> Result, MapBlockError> { if src.get_ref().len() - (src.position() as usize) < n { - Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, - "not enough bytes to fill buffer")) + // Corrupted length or otherwise not enough bytes to fill buffer. + Err(MapBlockError::BadData) } else { let mut bytes = vec_with_len(n); src.read_exact(&mut bytes)?; @@ -61,26 +67,28 @@ fn try_read_n(src: &mut Cursor<&[u8]>, n: usize) } -fn read_string16(src: &mut Cursor<&[u8]>) -> Result, std::io::Error> { +fn read_string16(src: &mut Cursor<&[u8]>) -> Result, MapBlockError> { let count = src.read_u16::()?; try_read_n(src, count as usize) } -fn read_string32(src: &mut Cursor<&[u8]>) -> Result, std::io::Error> { +fn read_string32(src: &mut Cursor<&[u8]>) -> Result, MapBlockError> { let count = src.read_u32::()?; try_read_n(src, count as usize) } fn write_string16(dst: &mut Cursor>, data: &[u8]) { - dst.write_u16::(data.len() as u16).unwrap(); + let len = u16::try_from(data.len()).unwrap(); + dst.write_u16::(len).unwrap(); dst.write(data).unwrap(); } fn write_string32(dst: &mut Cursor>, data: &[u8]) { - dst.write_u32::(data.len() as u32).unwrap(); + let len = u32::try_from(data.len()).unwrap(); + dst.write_u32::(len).unwrap(); dst.write(data).unwrap(); } @@ -89,14 +97,51 @@ fn write_string32(dst: &mut Cursor>, data: &[u8]) { mod tests { use super::*; + #[test] + #[should_panic] + fn test_string16_overflow() { + let mut buf = Cursor::new(Vec::new()); + let long = (0..128).collect::>().repeat(512); + write_string16(&mut buf, &long); + } + #[test] fn test_string_serialization() { - let buf = - b"\x00\x00\ - \x00\x0DHello, world!\ - \x00\x00\x00\x10more test data..\ - \x00\x00\x00\x00\ - \x00\x00\x00\x11corrupted length"; + let mut buf = Cursor::new(Vec::new()); + let long_string = b"lorem ipsum dolor sin amet ".repeat(10); + let huge_string = + b"There are only so many strings that have exactly 64 characters. " + .repeat(1024); + + write_string16(&mut buf, b""); + write_string16(&mut buf, &long_string); + write_string32(&mut buf, b""); + write_string32(&mut buf, &huge_string); + + let mut res = Vec::new(); + res.extend_from_slice(b"\x00\x00"); + res.extend_from_slice(b"\x01\x0E"); + res.extend_from_slice(&long_string); + res.extend_from_slice(b"\x00\x00\x00\x00"); + res.extend_from_slice(b"\x00\x01\x00\x00"); + res.extend_from_slice(&huge_string); + + assert_eq!(buf.into_inner(), res); + } + + #[test] + fn test_string_deserialization() { + let huge_string = + b"Magic purple goats can eat up to 30 kg of purple hay every day. " + .repeat(1024); + + let mut buf = Vec::new(); + buf.extend_from_slice(b"\x00\x00"); + buf.extend_from_slice(b"\x00\x0DHello, world!"); + buf.extend_from_slice(b"\x00\x01\x00\x00"); + buf.extend_from_slice(&huge_string); + buf.extend_from_slice(b"\x00\x00\x00\x00"); + let mut cursor = Cursor::new(&buf[..]); fn contains(res: Result, E>, val: &[u8]) -> bool { @@ -109,8 +154,30 @@ mod tests { assert!(contains(read_string16(&mut cursor), b"")); assert!(contains(read_string16(&mut cursor), b"Hello, world!")); - assert!(contains(read_string32(&mut cursor), b"more test data..")); + assert!(contains(read_string32(&mut cursor), &huge_string)); assert!(contains(read_string32(&mut cursor), b"")); - assert!(read_string32(&mut cursor).is_err()); + + let bad_string16s: &[&[u8]] = &[ + b"", + b"\xFF", + b"\x00\x01", + b"\x00\x2D actual data length < specified data length!", + ]; + for &bad in bad_string16s { + assert_eq!(read_string16(&mut Cursor::new(&bad)), + Err(MapBlockError::BadData)); + } + + let bad_string32s: &[&[u8]] = &[ + b"", + b"\x00\x00", + b"\x00\x00\x00\x01", + b"\xFF\xFF\xFF\xFF", + b"\x00\x00\x00\x2D actual data length < specified data length!", + ]; + for &bad in bad_string32s { + assert_eq!(read_string32(&mut Cursor::new(&bad)), + Err(MapBlockError::BadData)); + } } } diff --git a/src/map_block/name_id_map.rs b/src/map_block/name_id_map.rs index fa2c8a1..cc11e5a 100644 --- a/src/map_block/name_id_map.rs +++ b/src/map_block/name_id_map.rs @@ -14,7 +14,7 @@ impl NameIdMap { { let version = data.read_u8()?; if version != 0 { - return Err(MapBlockError::Other); + return Err(MapBlockError::InvalidSubVersion); } let count = data.read_u16::()? as usize; diff --git a/src/map_block/node_data.rs b/src/map_block/node_data.rs index 9178590..b8d9e4e 100644 --- a/src/map_block/node_data.rs +++ b/src/map_block/node_data.rs @@ -45,7 +45,7 @@ impl Compress for NodeData { let mut param2 = Vec::with_capacity(NODE_COUNT); decoder.read_to_end(&mut param2)?; if param2.len() != NODE_COUNT { - return Err(MapBlockError::DataError) + return Err(MapBlockError::BadData) } let total_in = decoder.total_in(); diff --git a/src/map_block/node_timer.rs b/src/map_block/node_timer.rs index 7dfa1f3..a0a1cb3 100644 --- a/src/map_block/node_timer.rs +++ b/src/map_block/node_timer.rs @@ -18,7 +18,7 @@ pub fn deserialize_timers(src: &mut Cursor<&[u8]>) { let data_len = src.read_u8()?; if data_len != 10 { - return Err(MapBlockError::Other); + return Err(MapBlockError::InvalidFeature); } let count = src.read_u16::()?; diff --git a/src/map_block/static_object.rs b/src/map_block/static_object.rs index a50bd08..34728cb 100644 --- a/src/map_block/static_object.rs +++ b/src/map_block/static_object.rs @@ -40,7 +40,7 @@ pub fn deserialize_objects(src: &mut Cursor<&[u8]>) { let version = src.read_u8()?; if version != 0 { - return Err(MapBlockError::Other); + return Err(MapBlockError::InvalidSubVersion); } let count = src.read_u16::()?; @@ -64,6 +64,10 @@ pub fn serialize_objects(objects: &StaticObjectList, dst: &mut Cursor>) } +/// Stores the name and data of a LuaEntity (Minetest's standard entity type). +/// +/// Relevant Minetest source file: src/server/luaentity_sao.cpp +#[derive(Debug)] pub struct LuaEntityData { pub name: Vec, pub data: Vec @@ -72,11 +76,12 @@ pub struct LuaEntityData { impl LuaEntityData { pub fn deserialize(src: &StaticObject) -> Result { if src.obj_type != 7 { - return Err(MapBlockError::Other); + return Err(MapBlockError::InvalidFeature); } let mut src_data = Cursor::new(src.data.as_slice()); if src_data.read_u8()? != 1 { - return Err(MapBlockError::Other); + // Unsupported LuaEntity version + return Err(MapBlockError::InvalidSubVersion); } let name = read_string16(&mut src_data)?; @@ -84,3 +89,38 @@ impl LuaEntityData { Ok(Self {name, data}) } } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lua_entity() { + let test_obj = StaticObject { + obj_type: 7, + f_pos: Vec3::new(4380, 17279, 32630), + data: b"\x01\x00\x0e__builtin:item\x00\x00\x00\x6e\ + return {[\"age\"] = 0.91899997927248478, \ + [\"itemstring\"] = \"basenodes:cobble 2\", \ + [\"dropped_by\"] = \"singleplayer\"}\ + \x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ + \x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00".to_vec() + }; + let entity = LuaEntityData::deserialize(&test_obj).unwrap(); + assert_eq!(entity.name, b"__builtin:item"); + assert_eq!(entity.data, + b"return {[\"age\"] = 0.91899997927248478, \ + [\"itemstring\"] = \"basenodes:cobble 2\", \ + [\"dropped_by\"] = \"singleplayer\"}"); + + let mut wrong_version = test_obj.clone(); + wrong_version.data[0] = 0; + assert_eq!(LuaEntityData::deserialize(&wrong_version).unwrap_err(), + MapBlockError::InvalidSubVersion); + + let wrong_type = StaticObject { obj_type: 6, ..test_obj }; + assert_eq!(LuaEntityData::deserialize(&wrong_type).unwrap_err(), + MapBlockError::InvalidFeature); + } +} diff --git a/src/time_keeper.rs b/src/testing.rs similarity index 68% rename from src/time_keeper.rs rename to src/testing.rs index e85162f..af30e8a 100644 --- a/src/time_keeper.rs +++ b/src/testing.rs @@ -49,3 +49,31 @@ impl TimeKeeper { status.log_info(msg); } } + + +pub fn debug_bytes(src: &[u8]) -> String { + let mut dst = String::new(); + for &byte in src { + if byte == b'\\' { + dst += "\\\\"; + } else if byte >= 32 && byte < 127 { + dst.push(byte as char); + } else { + dst += &format!("\\x{:0>2x}", byte); + } + } + dst +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_debug_bytes() { + let inp = b"\x00\x0a\x1f~~ Hello \\ World! ~~\x7f\xee\xff"; + let out = r"\x00\x0a\x1f~~ Hello \\ World! ~~\x7f\xee\xff"; + assert_eq!(&debug_bytes(&inp[..]), out); + } +} diff --git a/test_data/mapblock_v25.bin b/test_data/mapblock_v25.bin new file mode 100644 index 0000000000000000000000000000000000000000..511eddaae40135a3e7843f81db5d81668dbaa6cc GIT binary patch literal 347 zcmb1SW@4(C^Y-Rxt_B4W2gh95U;pcGM=^6ZiM*c^>8V>2Ah2}GrvpEiZ3Gng?lh{KJ|OUz9zNlZr=%w}wAWN2)GX$S)Y=R*btW}u;baNWfv`FW{8moq13 a7BMg(Nt9&frWSEAFfs}C1bZwzDix zoQ(Zq9ck~F{+g{#t(N~fSNFYf|DW|twef3j{ui{rTls&-|L*Vg^Ezk!j{kS-`}}`j z_jB&}zmDmr&7c2$EMB$q52KrVl69Ks+@@dWD&mqE7#J9Vkb#Mvfzbl!iuef(j8_gY z*fBDQm!uY##OLOxSmh<=rj{h8$EW6%WR_F{b+DS6TNoQNZ~>Vp0B8;burn~I3otPF zDS&YxBZDa1u;l!l{33)=QbnmHrA2uP)zL~2nY_|mrC0@91;biA<})yG9Ra!-Xc-^e i;F8ST)FPl)m=iOLxEPrD{{!7=2~-pTauSHeZ~_1lT5S>l literal 0 HcmV?d00001 diff --git a/test_data/test_mod/init.lua b/test_data/test_mod/init.lua new file mode 100644 index 0000000..528c318 --- /dev/null +++ b/test_data/test_mod/init.lua @@ -0,0 +1,91 @@ +-- THIS MOD IS NOT USEFUL TO USERS! + +-- test_mod defines a few nodes and entities which may be used to generate test +-- map data for MapEditr. + + +local tex = "test_mod_test.png" +local colors = { + "#FF0000", "#FFFF00", "#00FF00", "#00FFFF", "#0000FF", "#FF00FF" +} + + +minetest.register_node("test_mod:stone", { + drawtype = "normal", + tiles = {"default_stone.png^[colorize:#3FFF3F:63"}, + groups = {oddly_breakable_by_hand = 3}, +}) + + +minetest.register_node("test_mod:timer", { + drawtype = "nodebox", + node_box = { + type = "fixed", + fixed = {-1/4, -1/2, -1/4, 1/4, 1/4, 1/4} + }, + tiles = {tex}, + paramtype = "light", + paramtype2 = "facedir", + groups = {oddly_breakable_by_hand = 3}, + + on_construct = function(pos) + minetest.get_node_timer(pos):start(1.337) + end, + + on_timer = function(pos, elapsed) + local node = minetest.get_node(pos) + node.param2 = (node.param2 + 4) % 24 + + minetest.set_node(pos, node) + minetest.get_node_timer(pos):start(1.337) + end, +}) + + +minetest.register_entity("test_mod:color_entity", { + initial_properties = { + visual = "cube", + textures = {tex, tex, tex, tex, tex, tex}, + }, + + on_activate = function(self, staticdata, dtime_s) + if staticdata and staticdata ~= "" then + t = minetest.deserialize(staticdata) + self._color_num = t.color_num + else + self._color_num = math.random(1, #colors) + end + + self.object:settexturemod( + "^[colorize:" .. colors[self._color_num] .. ":127") + end, + + get_staticdata = function(self) + return minetest.serialize({color_num = self._color_num}) + end, +}) + + +minetest.register_entity("test_mod:nametag_entity", { + initial_properties = { + visual = "sprite", + textures = {tex}, + }, + + on_activate = function(self, staticdata, dtime_s) + if staticdata and staticdata ~= "" then + self._text = staticdata + else + self._text = tostring(math.random(0, 999999)) + end + + self.object:set_nametag_attributes({ + text = self._text, + color = "#FFFF00" + }) + end, + + get_staticdata = function(self) + return self._text + end, +}) diff --git a/test_data/test_mod/textures/test_mod_test.png b/test_data/test_mod/textures/test_mod_test.png new file mode 100644 index 0000000000000000000000000000000000000000..e15559489dcf0f794627136f8faac001c47ad10c GIT binary patch literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!93?!50ihlx9JOMr-u3rA3>$V?w{`~pZ&oA9{ z54Hfs?LA!_LpZJ{N3gQ+{Qm#{|M3l`CMLa!w^xPUR$z%(w6L?YGcj;_nE2sEYHA7> rw{o0fI^A^j?Z5y3r9JW`B6t{tU#TVM`Pc6N8pYu0>gTe~DWM4f6Ad(6 literal 0 HcmV?d00001