Completion Part 6

The Final Frontier?
master
random-geek 2021-05-18 00:11:01 -07:00
parent 413f6e2579
commit dcf8de18eb
13 changed files with 523 additions and 33 deletions

View File

@ -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;

View File

@ -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::<BigEndian>()? }
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<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(&copy).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);
}
}
}

View File

@ -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);
}
}

View File

@ -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<std::io::Error> for MapBlockError {
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`.
#[inline(always)]
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 {
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<Vec<u8>, std::io::Error> {
fn read_string16(src: &mut Cursor<&[u8]>) -> Result<Vec<u8>, MapBlockError> {
let count = src.read_u16::<BigEndian>()?;
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>()?;
try_read_n(src, count as usize)
}
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();
}
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();
}
@ -89,14 +97,51 @@ fn write_string32(dst: &mut Cursor<Vec<u8>>, 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::<Vec<u8>>().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<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"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));
}
}
}

View File

@ -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::<BigEndian>()? as usize;

View File

@ -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();

View File

@ -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::<BigEndian>()?;

View File

@ -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::<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 name: Vec<u8>,
pub data: Vec<u8>
@ -72,11 +76,12 @@ pub struct LuaEntityData {
impl LuaEntityData {
pub fn deserialize(src: &StaticObject) -> Result<Self, MapBlockError> {
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);
}
}

View File

@ -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);
}
}

BIN
test_data/mapblock_v25.bin Normal file

Binary file not shown.

BIN
test_data/mapblock_v28.bin Normal file

Binary file not shown.

View 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,
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B