Completion Part 6
The Final Frontier?
This commit is contained in:
parent
413f6e2579
commit
dcf8de18eb
@ -1,5 +1,5 @@
|
|||||||
// Kept for testing purposes
|
// Uncomment if needed for testing
|
||||||
// mod time_keeper;
|
// mod testing;
|
||||||
mod spatial;
|
mod spatial;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod map_database;
|
mod map_database;
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
use super::*;
|
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 MIN_BLOCK_VER: u8 = 25;
|
||||||
const MAX_BLOCK_VER: u8 = 28;
|
const MAX_BLOCK_VER: u8 = 28;
|
||||||
const BLOCK_BUF_SIZE: usize = 2048;
|
const BLOCK_BUF_SIZE: usize = 2048;
|
||||||
@ -34,7 +42,7 @@ impl MapBlock {
|
|||||||
// Version
|
// Version
|
||||||
let version = data.read_u8()?;
|
let version = data.read_u8()?;
|
||||||
if version < MIN_BLOCK_VER || version > MAX_BLOCK_VER {
|
if version < MIN_BLOCK_VER || version > MAX_BLOCK_VER {
|
||||||
return Err(MapBlockError::InvalidVersion);
|
return Err(MapBlockError::InvalidBlockVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
@ -43,13 +51,14 @@ impl MapBlock {
|
|||||||
// Light data
|
// Light data
|
||||||
let lighting_complete =
|
let lighting_complete =
|
||||||
if version >= 27 { data.read_u16::<BigEndian>()? }
|
if version >= 27 { data.read_u16::<BigEndian>()? }
|
||||||
else { 0 };
|
else { 0xFFFF };
|
||||||
|
|
||||||
// Content width/param width
|
// Content width/param width
|
||||||
let content_width = data.read_u8()?;
|
let content_width = data.read_u8()?;
|
||||||
let params_width = data.read_u8()?;
|
let params_width = data.read_u8()?;
|
||||||
|
// TODO: support content_width == 1?
|
||||||
if content_width != 2 || params_width != 2 {
|
if content_width != 2 || params_width != 2 {
|
||||||
return Err(MapBlockError::Other);
|
return Err(MapBlockError::InvalidFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node data
|
// Node data
|
||||||
@ -121,3 +130,179 @@ impl MapBlock {
|
|||||||
buf
|
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<Vec<u8>> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -33,9 +33,11 @@ impl NodeMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let end_finder = TwoWaySearcher::new(END_STR);
|
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
|
let end = end_finder
|
||||||
.search_in(&data.get_ref()[data.position() as usize ..])
|
.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());
|
let mut inv = vec_with_len(end + END_STR.len());
|
||||||
data.read_exact(&mut inv)?;
|
data.read_exact(&mut inv)?;
|
||||||
@ -79,7 +81,7 @@ impl NodeMetadataListExt for NodeMetadataList {
|
|||||||
|
|
||||||
let version = data.read_u8()?;
|
let version = data.read_u8()?;
|
||||||
if version > 2 {
|
if version > 2 {
|
||||||
return Err(MapBlockError::InvalidVersion)
|
return Err(MapBlockError::InvalidSubVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = match version {
|
let count = match version {
|
||||||
@ -121,3 +123,80 @@ impl NodeMetadataListExt for NodeMetadataList {
|
|||||||
data.into_inner()
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::io::prelude::*;
|
use std::io::prelude::*;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use byteorder::{ByteOrder, BigEndian, ReadBytesExt, WriteBytesExt};
|
use byteorder::{ByteOrder, BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
@ -23,16 +24,21 @@ use node_timer::{serialize_timers, deserialize_timers};
|
|||||||
pub use name_id_map::NameIdMap;
|
pub use name_id_map::NameIdMap;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum MapBlockError {
|
pub enum MapBlockError {
|
||||||
InvalidVersion,
|
/// Block data is malformed or missing.
|
||||||
DataError,
|
BadData,
|
||||||
Other,
|
/// 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<std::io::Error> for MapBlockError {
|
impl From<std::io::Error> for MapBlockError {
|
||||||
fn from(_: std::io::Error) -> Self {
|
fn from(_: std::io::Error) -> Self {
|
||||||
Self::DataError
|
Self::BadData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,11 +54,11 @@ fn vec_with_len<T>(len: usize) -> Vec<T> {
|
|||||||
/// enough bytes in `src`.
|
/// enough bytes in `src`.
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn try_read_n(src: &mut Cursor<&[u8]>, n: usize)
|
fn try_read_n(src: &mut Cursor<&[u8]>, n: usize)
|
||||||
-> Result<Vec<u8>, std::io::Error>
|
-> Result<Vec<u8>, MapBlockError>
|
||||||
{
|
{
|
||||||
if src.get_ref().len() - (src.position() as usize) < n {
|
if src.get_ref().len() - (src.position() as usize) < n {
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof,
|
// Corrupted length or otherwise not enough bytes to fill buffer.
|
||||||
"not enough bytes to fill buffer"))
|
Err(MapBlockError::BadData)
|
||||||
} else {
|
} else {
|
||||||
let mut bytes = vec_with_len(n);
|
let mut bytes = vec_with_len(n);
|
||||||
src.read_exact(&mut bytes)?;
|
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<Vec<u8>, std::io::Error> {
|
fn read_string16(src: &mut Cursor<&[u8]>) -> Result<Vec<u8>, MapBlockError> {
|
||||||
let count = src.read_u16::<BigEndian>()?;
|
let count = src.read_u16::<BigEndian>()?;
|
||||||
try_read_n(src, count as usize)
|
try_read_n(src, count as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn read_string32(src: &mut Cursor<&[u8]>) -> Result<Vec<u8>, std::io::Error> {
|
fn read_string32(src: &mut Cursor<&[u8]>) -> Result<Vec<u8>, MapBlockError> {
|
||||||
let count = src.read_u32::<BigEndian>()?;
|
let count = src.read_u32::<BigEndian>()?;
|
||||||
try_read_n(src, count as usize)
|
try_read_n(src, count as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn write_string16(dst: &mut Cursor<Vec<u8>>, data: &[u8]) {
|
fn write_string16(dst: &mut Cursor<Vec<u8>>, data: &[u8]) {
|
||||||
dst.write_u16::<BigEndian>(data.len() as u16).unwrap();
|
let len = u16::try_from(data.len()).unwrap();
|
||||||
|
dst.write_u16::<BigEndian>(len).unwrap();
|
||||||
dst.write(data).unwrap();
|
dst.write(data).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn write_string32(dst: &mut Cursor<Vec<u8>>, data: &[u8]) {
|
fn write_string32(dst: &mut Cursor<Vec<u8>>, data: &[u8]) {
|
||||||
dst.write_u32::<BigEndian>(data.len() as u32).unwrap();
|
let len = u32::try_from(data.len()).unwrap();
|
||||||
|
dst.write_u32::<BigEndian>(len).unwrap();
|
||||||
dst.write(data).unwrap();
|
dst.write(data).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,14 +97,51 @@ fn write_string32(dst: &mut Cursor<Vec<u8>>, data: &[u8]) {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn test_string16_overflow() {
|
||||||
|
let mut buf = Cursor::new(Vec::new());
|
||||||
|
let long = (0..128).collect::<Vec<u8>>().repeat(512);
|
||||||
|
write_string16(&mut buf, &long);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_string_serialization() {
|
fn test_string_serialization() {
|
||||||
let buf =
|
let mut buf = Cursor::new(Vec::new());
|
||||||
b"\x00\x00\
|
let long_string = b"lorem ipsum dolor sin amet ".repeat(10);
|
||||||
\x00\x0DHello, world!\
|
let huge_string =
|
||||||
\x00\x00\x00\x10more test data..\
|
b"There are only so many strings that have exactly 64 characters. "
|
||||||
\x00\x00\x00\x00\
|
.repeat(1024);
|
||||||
\x00\x00\x00\x11corrupted length";
|
|
||||||
|
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[..]);
|
let mut cursor = Cursor::new(&buf[..]);
|
||||||
|
|
||||||
fn contains<E>(res: Result<Vec<u8>, E>, val: &[u8]) -> bool {
|
fn contains<E>(res: Result<Vec<u8>, E>, val: &[u8]) -> bool {
|
||||||
@ -109,8 +154,30 @@ mod tests {
|
|||||||
|
|
||||||
assert!(contains(read_string16(&mut cursor), b""));
|
assert!(contains(read_string16(&mut cursor), b""));
|
||||||
assert!(contains(read_string16(&mut cursor), b"Hello, world!"));
|
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!(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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ impl NameIdMap {
|
|||||||
{
|
{
|
||||||
let version = data.read_u8()?;
|
let version = data.read_u8()?;
|
||||||
if version != 0 {
|
if version != 0 {
|
||||||
return Err(MapBlockError::Other);
|
return Err(MapBlockError::InvalidSubVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = data.read_u16::<BigEndian>()? as usize;
|
let count = data.read_u16::<BigEndian>()? as usize;
|
||||||
|
@ -45,7 +45,7 @@ impl Compress for NodeData {
|
|||||||
let mut param2 = Vec::with_capacity(NODE_COUNT);
|
let mut param2 = Vec::with_capacity(NODE_COUNT);
|
||||||
decoder.read_to_end(&mut param2)?;
|
decoder.read_to_end(&mut param2)?;
|
||||||
if param2.len() != NODE_COUNT {
|
if param2.len() != NODE_COUNT {
|
||||||
return Err(MapBlockError::DataError)
|
return Err(MapBlockError::BadData)
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_in = decoder.total_in();
|
let total_in = decoder.total_in();
|
||||||
|
@ -18,7 +18,7 @@ pub fn deserialize_timers(src: &mut Cursor<&[u8]>)
|
|||||||
{
|
{
|
||||||
let data_len = src.read_u8()?;
|
let data_len = src.read_u8()?;
|
||||||
if data_len != 10 {
|
if data_len != 10 {
|
||||||
return Err(MapBlockError::Other);
|
return Err(MapBlockError::InvalidFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = src.read_u16::<BigEndian>()?;
|
let count = src.read_u16::<BigEndian>()?;
|
||||||
|
@ -40,7 +40,7 @@ pub fn deserialize_objects(src: &mut Cursor<&[u8]>)
|
|||||||
{
|
{
|
||||||
let version = src.read_u8()?;
|
let version = src.read_u8()?;
|
||||||
if version != 0 {
|
if version != 0 {
|
||||||
return Err(MapBlockError::Other);
|
return Err(MapBlockError::InvalidSubVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
let count = src.read_u16::<BigEndian>()?;
|
let count = src.read_u16::<BigEndian>()?;
|
||||||
@ -64,6 +64,10 @@ pub fn serialize_objects(objects: &StaticObjectList, dst: &mut Cursor<Vec<u8>>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 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 struct LuaEntityData {
|
||||||
pub name: Vec<u8>,
|
pub name: Vec<u8>,
|
||||||
pub data: Vec<u8>
|
pub data: Vec<u8>
|
||||||
@ -72,11 +76,12 @@ pub struct LuaEntityData {
|
|||||||
impl LuaEntityData {
|
impl LuaEntityData {
|
||||||
pub fn deserialize(src: &StaticObject) -> Result<Self, MapBlockError> {
|
pub fn deserialize(src: &StaticObject) -> Result<Self, MapBlockError> {
|
||||||
if src.obj_type != 7 {
|
if src.obj_type != 7 {
|
||||||
return Err(MapBlockError::Other);
|
return Err(MapBlockError::InvalidFeature);
|
||||||
}
|
}
|
||||||
let mut src_data = Cursor::new(src.data.as_slice());
|
let mut src_data = Cursor::new(src.data.as_slice());
|
||||||
if src_data.read_u8()? != 1 {
|
if src_data.read_u8()? != 1 {
|
||||||
return Err(MapBlockError::Other);
|
// Unsupported LuaEntity version
|
||||||
|
return Err(MapBlockError::InvalidSubVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = read_string16(&mut src_data)?;
|
let name = read_string16(&mut src_data)?;
|
||||||
@ -84,3 +89,38 @@ impl LuaEntityData {
|
|||||||
Ok(Self {name, data})
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -49,3 +49,31 @@ impl TimeKeeper {
|
|||||||
status.log_info(msg);
|
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);
|
||||||
|
}
|
||||||
|
}
|
BIN
test_data/mapblock_v25.bin
Normal file
BIN
test_data/mapblock_v25.bin
Normal file
Binary file not shown.
BIN
test_data/mapblock_v28.bin
Normal file
BIN
test_data/mapblock_v28.bin
Normal file
Binary file not shown.
91
test_data/test_mod/init.lua
Normal file
91
test_data/test_mod/init.lua
Normal file
@ -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,
|
||||||
|
})
|
BIN
test_data/test_mod/textures/test_mod_test.png
Normal file
BIN
test_data/test_mod/textures/test_mod_test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 144 B |
Loading…
x
Reference in New Issue
Block a user