vintage_schematics/entity/
mod.rs

1//! Vintage Story [block entity](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.BlockEntity.html)
2//! functionality.
3
4use color_eyre::{
5	eyre,
6	eyre::{Context, OptionExt, bail, eyre},
7};
8use itertools::Itertools;
9use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
10use serde_repr::{Deserialize_repr, Serialize_repr};
11
12use crate::{Map, ascii85, formats::internal::EntityMap};
13
14#[cfg(feature = "miniserde")]
15mod miniserde;
16
17/// The maximum number of entries that entity encoding and decoding will preallocate space for.
18/// This value is capped to mitigate memory exhaustion caused by invalid data.
19///
20/// This value is only used when preallocating with [`Vec::with_capacity`].
21/// Memory usage is unbounded in the case of valid large inputs.
22const MAX_PREALLOC: usize = 1024;
23
24/// Mapping of block entity IDs to their associated [`EntityMap`]s.
25pub type BlockEntityMap = Map<u32, EntityMap>;
26
27/// A [tree attribute](https://apidocs.vintagestory.at/api/Vintagestory.API.Datastructures.TreeAttribute.html) value.
28///
29/// This is a tagged value that can represent various types of data.
30#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
31pub enum Value {
32	// https://github.com/anegostudios/vsapi/blob/f0d94e1/Datastructures/AttributeTree/TreeAttribute.cs#L141
33	Integer(i32),
34	Long(i64),
35	Double(f64),
36	Float(f32),
37	String(String),
38	Tree(Map<String, Self>),
39	ItemStack(Option<ItemStack>),
40	ByteArray(Vec<u8>),
41	Boolean(bool),
42	StringArray(Vec<String>),
43	IntArray(Vec<i32>),
44	FloatArray(Vec<f32>),
45	DoubleArray(Vec<f64>),
46	TreeArray(Vec<Map<String, Self>>),
47	LongArray(Vec<i64>),
48	BoolArray(Vec<bool>),
49}
50
51/// An [`ItemStack`](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.ItemStack.html).
52#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
53pub struct ItemStack {
54	/// Whether this `ItemStack` consists of a block or an item.
55	pub class: ItemClass,
56
57	/// The block or item's ID.
58	pub id: i32,
59
60	/// The number of blocks or items in this stack.
61	pub stack_size: i32,
62
63	/// [Item stack attributes](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.ItemStack.html#Vintagestory_API_Common_ItemStack_Attributes).
64	pub stack_attributes: EntityMap,
65}
66
67impl ItemStack {
68	/// Encodes the `ItemStack` as an array of bytes.
69	fn encode(&self) -> Vec<u8> {
70		let mut output = Vec::with_capacity(16);
71		output.push(0);
72		output.extend((self.class as i32).to_le_bytes());
73		output.extend(self.id.to_le_bytes());
74		output.extend(self.stack_size.to_le_bytes());
75		output.extend(encode_entities(&self.stack_attributes));
76
77		output
78	}
79}
80
81/// An [`ItemStack`]'s [item class](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.EnumItemClass.html).
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize_repr, Serialize_repr)]
83#[repr(i32)]
84pub enum ItemClass {
85	/// This is a [block](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.Block.html).
86	Block = 0,
87
88	/// This is an
89	/// [item](https://apidocs.vintagestory.at/api/Vintagestory.API.Common.Item.html)[.](https://youtu.be/NGe79n-jcHk?t=873)
90	Item = 1,
91}
92
93impl TryFrom<i32> for ItemClass {
94	type Error = eyre::Error;
95
96	fn try_from(value: i32) -> Result<Self, Self::Error> {
97		match value {
98			0 => Ok(Self::Block),
99			1 => Ok(Self::Item),
100			_ => Err(eyre!("unknown item class: {value}")),
101		}
102	}
103}
104
105impl Value {
106	/// The expected length of a value of this type.
107	///
108	/// Only fixed-length types (such as integers) have expected lengths.
109	///
110	/// | Value          | Expected length |
111	/// | -------------- | --------------- |
112	/// | Integer, Float | 4 bytes         |
113	/// | Long, Double   | 8 bytes         |
114	/// | Boolean        | 1 byte          |
115	/// | Other          | `None`          |
116	#[must_use]
117	pub const fn expected_length(&self) -> Option<usize> {
118		match self {
119			Self::Integer(_) | Self::Float(_) => Some(4),
120			Self::Long(_) | Self::Double(_) => Some(8),
121			Self::Boolean(_) => Some(1),
122			_ => None,
123		}
124	}
125
126	/// The byte used to identify this value type in Vintage Story block entity data.
127	#[must_use]
128	pub const fn tag_byte(&self) -> u8 {
129		match self {
130			Self::Integer(_) => 1,
131			Self::Long(_) => 2,
132			Self::Double(_) => 3,
133			Self::Float(_) => 4,
134			Self::String(_) => 5,
135			Self::Tree(_) => 6,
136			Self::ItemStack(_) => 7,
137			Self::ByteArray(_) => 8,
138			Self::Boolean(_) => 9,
139			Self::StringArray(_) => 10,
140			Self::IntArray(_) => 11,
141			Self::FloatArray(_) => 12,
142			Self::DoubleArray(_) => 13,
143			Self::TreeArray(_) => 14,
144			Self::LongArray(_) => 15,
145			Self::BoolArray(_) => 16,
146		}
147	}
148
149	/// Encodes the value for use in Vintage Story block entity data.
150	/// Note that Vintage Story encodes this data as [ASCII85](ascii85) when storing it in world edit JSON files.
151	///
152	/// # Panics
153	///
154	/// Panics if the provided value is invalid, such as a `Value::String` longer than [`i32::MAX`] bytes.
155	pub fn encode(&self) -> Vec<u8> {
156		match self {
157			Self::Integer(i) => i.to_le_bytes().to_vec(),
158			Self::Long(l) => l.to_le_bytes().to_vec(),
159			Self::Double(d) => d.to_le_bytes().to_vec(),
160			Self::Float(f) => f.to_le_bytes().to_vec(),
161			Self::String(s) => {
162				let mut output = encode_seven_bit_int(i32::try_from(s.len()).expect("string is too long"));
163				output.extend(s.as_bytes());
164				output
165			}
166			Self::Tree(t) => encode_entities(t),
167			Self::ByteArray(b) => {
168				let length = u16::try_from(b.len()).expect("byte arrays should contain less than 65,535 bytes");
169				let mut output = Vec::with_capacity(MAX_PREALLOC.min(b.len() + 2));
170				output.extend(length.to_le_bytes().iter());
171				output.extend(b);
172				output
173			}
174			Self::ItemStack(i) => i.as_ref().map_or_else(|| vec![1], ItemStack::encode),
175			Self::Boolean(b) => vec![u8::from(*b)],
176			Self::StringArray(s) => {
177				let mut output = Vec::with_capacity(MAX_PREALLOC.min(s.iter().map(|s| s.len() + 1).sum::<usize>() + 1));
178
179				#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
180				output.extend(encode_seven_bit_int(s.len() as i32));
181
182				for s in s {
183					output.extend(Self::String(s.clone()).encode());
184				}
185
186				output
187			}
188			Self::IntArray(i) => encode_array(i),
189			Self::FloatArray(f) => encode_array(f),
190			Self::DoubleArray(d) => encode_array(d),
191			Self::TreeArray(_t) => unimplemented!("tree arrays are unsupported"),
192			Self::LongArray(l) => encode_array(l),
193			Self::BoolArray(b) => {
194				let bytes = b.iter().map(|b| u8::from(*b)).collect::<Box<_>>();
195				encode_array(&bytes)
196			}
197		}
198	}
199
200	/// Is this value a `Value::String`?
201	const fn is_string(&self) -> bool { matches!(self, Self::String(_)) }
202
203	/// Is this value a `Value::Tree`?
204	const fn is_tree(&self) -> bool { matches!(self, Self::Tree(_)) }
205
206	/// Is this value a `Value::StringArray`?
207	const fn is_string_array(&self) -> bool { matches!(self, Self::StringArray(_)) }
208
209	/// Is this value a `Value::ItemStack`?
210	const fn is_item_stack(&self) -> bool { matches!(self, Self::ItemStack(_)) }
211
212	/// Is this value a `Value::ByteArray`?
213	const fn is_byte_array(&self) -> bool { matches!(self, Self::ByteArray(_)) }
214}
215
216impl TryFrom<u8> for Value {
217	type Error = eyre::Error;
218
219	fn try_from(value: u8) -> Result<Self, Self::Error> {
220		match value {
221			1 => Ok(Self::Integer(0)),
222			2 => Ok(Self::Long(0)),
223			3 => Ok(Self::Double(0.0)),
224			4 => Ok(Self::Float(0.0)),
225			5 => Ok(Self::String(String::new())),
226			6 => Ok(Self::Tree(Map::new())),
227			7 => Ok(Self::ItemStack(None)),
228			8 => Ok(Self::ByteArray(Vec::new())),
229			9 => Ok(Self::Boolean(false)),
230			10 => Ok(Self::StringArray(Vec::new())),
231			11 => Ok(Self::IntArray(Vec::new())),
232			12 => Ok(Self::FloatArray(Vec::new())),
233			13 => Ok(Self::DoubleArray(Vec::new())),
234			14 => Ok(Self::TreeArray(Vec::new())),
235			15 => Ok(Self::LongArray(Vec::new())),
236			16 => Ok(Self::BoolArray(Vec::new())),
237			_ => Err(eyre!("unknown kind (not in range 1..=16): {value}")),
238		}
239	}
240}
241
242impl Default for Value {
243	fn default() -> Self { Self::Integer(0) }
244}
245
246#[derive(Debug, Clone, Default)]
247/// Vintage Story block entity data.
248pub struct BlockEntities(pub BlockEntityMap);
249
250impl<'de> Deserialize<'de> for BlockEntities {
251	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
252	where
253		D: Deserializer<'de>,
254	{
255		let map: Map<u32, String> = Deserialize::deserialize(deserializer)?;
256		Ok(Self(
257			map
258				.into_iter()
259				.map(|(id, value)| parse_encoded_entities(&value).map(|v| (id, v)))
260				.collect::<Result<_, _>>()
261				.map_err(D::Error::custom)?,
262		))
263	}
264}
265
266impl Serialize for BlockEntities {
267	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
268	where
269		S: Serializer,
270	{
271		let map: Map<u32, String> = self
272			.0
273			.iter()
274			.map(|(&id, entities)| (id, ascii85::encode(&encode_entities(entities), false)))
275			.collect();
276		map.serialize(serializer)
277	}
278}
279
280/// Parse [ASCII85-encoded](ascii85) Vintage Story block entity data.
281///
282/// # Errors
283///
284/// Returns an error if the provided string is invalid ASCII85 (see [`ascii85::decode`]), or if it cannot be decoded
285/// as an [`EntityMap`] by [`parse_entities`].
286pub fn parse_encoded_entities(input: &str) -> eyre::Result<EntityMap> {
287	let mut input = ascii85::decode(input)?.into_iter();
288	let input = &mut input;
289
290	parse_entities(input)
291}
292
293/// Parse Vintage Story block entity data.
294///
295/// # Errors
296///
297/// Returns an error if the input terminates early, contains non-UTF8 strings, or is otherwise invalid.
298pub fn parse_entities(input: &mut impl Iterator<Item = u8>) -> eyre::Result<EntityMap> {
299	let mut map = Map::new();
300
301	// entity data is encoded like this:
302	// - value type tag (e.g. 0x01 for a 32-bit int)
303	// - one or more bytes for field name length (see `parse_string`)
304	// - field name as UTF-8(?) string
305	// - value bytes
306
307	while let Some(&byte) = input.next().as_ref() {
308		// value type
309		// if it's null, this is the end of the input
310		if byte == 0 {
311			break;
312		}
313
314		// name
315		let name = parse_string(input)?;
316
317		// value
318		let mut value = Value::try_from(byte)?;
319
320		value = if value.is_tree() {
321			Value::Tree(parse_entities(input)?)
322		} else if value.is_string_array() {
323			let count = i32::from_le_bytes(input.next_array().ok_or_eyre("EOF while parsing array length")?);
324
325			#[allow(clippy::cast_sign_loss)]
326			let mut strings = Vec::with_capacity(MAX_PREALLOC.min(count as usize));
327			for _ in 0..count {
328				strings.push(parse_string(input)?);
329			}
330
331			Value::StringArray(strings)
332		} else if value.is_item_stack() {
333			Value::ItemStack(parse_item_stack(input)?)
334		} else if value.is_byte_array() {
335			let length = u16::from_le_bytes(input.next_array().ok_or_eyre("byte array length should be two bytes")?) as usize;
336			Value::ByteArray(input.take(length).collect())
337		} else {
338			let value_length = value
339				.expected_length()
340				.map_or_else(
341					|| {
342						if value.is_string() {
343							usize::try_from(parse_seven_bit_int(input)?)
344						} else {
345							usize::try_from(i32::from_le_bytes(input.next_array().ok_or_eyre("EOF while parsing array length")?))
346						}
347						.context("invalid value for string/array length")
348					},
349					Ok,
350				)
351				.context("lengths should be positive")?;
352
353			let data: Vec<u8> = input.take(value_length).collect();
354			match value {
355				// all numeric types are little-endian:
356				// https://learn.microsoft.com/en-au/dotnet/api/system.io.binarywriter.write
357				Value::Integer(_) => {
358					Value::Integer(i32::from_le_bytes(data.try_into().map_err(|_| eyre!("EOF while parsing integer"))?))
359				}
360				Value::Long(_) => {
361					Value::Long(i64::from_le_bytes(data.try_into().map_err(|_| eyre!("EOF while parsing long"))?))
362				}
363				Value::Double(_) => {
364					Value::Double(f64::from_le_bytes(data.try_into().map_err(|_| eyre!("EOF while parsing double"))?))
365				}
366				Value::Float(_) => {
367					Value::Float(f32::from_le_bytes(data.try_into().map_err(|_| eyre!("EOF while parsing float"))?))
368				}
369				Value::Boolean(_) => Value::Boolean(*data.first().ok_or_eyre("EOF while parsing bool")? != 0),
370				Value::String(_) => Value::String(String::from_utf8(data).context("invalid string value")?),
371				Value::IntArray(_) => Value::IntArray(parse_array(&mut data.into_iter(), value_length)?),
372				Value::FloatArray(_) => Value::FloatArray(parse_array(&mut data.into_iter(), value_length)?),
373				Value::DoubleArray(_) => Value::DoubleArray(parse_array(&mut data.into_iter(), value_length)?),
374				Value::TreeArray(_) => bail!("tree arrays are unsupported"),
375				Value::LongArray(_) => Value::LongArray(parse_array(&mut data.into_iter(), value_length)?),
376				Value::BoolArray(_) => {
377					let array = parse_array::<u8>(&mut data.into_iter(), value_length)?;
378					Value::BoolArray(array.iter().map(|b| *b != 0).collect::<Vec<bool>>())
379				}
380
381				Value::Tree(_) | Value::StringArray(_) | Value::ItemStack(_) | Value::ByteArray(_) => {
382					unreachable!("should have been parsed already")
383				}
384			}
385		};
386
387		map.insert(name, value);
388	}
389
390	Ok(map)
391}
392
393/// Parses an array of values of type `T`.
394///
395/// # Errors
396///
397/// Returns an error if the input is less than `size_of::<T>() * length` bytes long.
398// h/t https://stackoverflow.com/a/77959838
399pub fn parse_array<T>(input: &mut dyn Iterator<Item = u8>, length: usize) -> eyre::Result<Vec<T>>
400where
401	T: num_traits::FromBytes,
402	for<'a> &'a [u8]: TryInto<&'a T::Bytes>,
403{
404	let mut output = Vec::with_capacity(MAX_PREALLOC.min(length));
405	for _ in 0..length {
406		let bytes = input.take(size_of::<T>()).collect::<Vec<u8>>();
407		let bytes = bytes
408			.chunks_exact(size_of::<T>())
409			.next()
410			.ok_or_else(|| eyre!("unexpected EOF while parsing {}", std::any::type_name::<T>()))?;
411		let bytes =
412			TryInto::<&T::Bytes>::try_into(bytes).map_err(|_| eyre!("couldn't convert slice to num_traits::Bytes"))?;
413		output.push(T::from_le_bytes(bytes));
414	}
415
416	Ok(output)
417}
418
419/// Encodes an array of `T` as a length-prefixed array of little endian `T`s.
420/// The length prefix is a 32-bit little endian *signed* integer.
421///
422/// # Panics
423///
424/// Panics if `array` contains more than `i32::MAX` items.
425pub fn encode_array<T>(array: &[T]) -> Vec<u8>
426where
427	T: num_traits::ToBytes,
428{
429	let mut output = Vec::with_capacity(size_of_val(array) + 4);
430
431	output.extend_from_slice(
432		&i32::try_from(array.len()).expect("arrays should contain less than i32::MAX items").to_le_bytes(),
433	);
434
435	for item in array {
436		output.extend_from_slice(item.to_le_bytes().as_ref());
437	}
438
439	output
440}
441
442/// Parses an [`ItemStack`].
443///
444/// # Errors
445///
446/// Returns an error if the input iterator is empty, or if any of the fields contain invalid values or terminate early.
447pub fn parse_item_stack(input: &mut impl Iterator<Item = u8>) -> eyre::Result<Option<ItemStack>> {
448	if input.next().ok_or_eyre("EOF while parsing item stack")? == 1 {
449		// empty stack
450		Ok(None)
451	} else {
452		Ok(Some(ItemStack {
453			class: ItemClass::try_from(i32::from_le_bytes(input.next_array().ok_or_eyre("EOF while parsing item class")?))?,
454			id: i32::from_le_bytes(input.next_array().ok_or_eyre("EOF while parsing item id")?),
455			stack_size: i32::from_le_bytes(input.next_array().ok_or_eyre("EOF while parsing item stack size")?),
456			stack_attributes: parse_entities(input)?,
457		}))
458	}
459}
460
461/// Parses a string with a ["seven bit int" length prefix](parse_seven_bit_int).
462///
463/// # Errors
464///
465/// Returns an error if the length prefix could not be parsed by [`parse_seven_bit_int`], the length is negative, or
466/// the string contains invalid UTF-8.
467pub fn parse_string(input: &mut dyn Iterator<Item = u8>) -> eyre::Result<String> {
468	let length = usize::try_from(parse_seven_bit_int(input)?).context("string length should be positive")?;
469	if length == 0 {
470		bail!("string length should be non-zero")
471	}
472	String::from_utf8(input.take(length).collect()).context("invalid string")
473}
474
475/// Parses a "seven bit integer" as read by
476/// [C#'s `BinaryReader`](https://learn.microsoft.com/en-au/dotnet/api/system.io.binaryreader.readstring).
477///
478/// # Errors
479///
480/// Returns an error if the provided value would exceed [`i32::MAX`].
481pub fn parse_seven_bit_int(input: &mut dyn Iterator<Item = u8>) -> eyre::Result<i32> {
482	// strings are encoded with C#'s `BinaryReader`:
483	// https://github.com/anegostudios/vsapi/blob/f0d94e1/Datastructures/AttributeTree/StringAttribute.cs#L25
484	// `BinaryReader` strings are prefixed with a length "encoded as an integer seven bits at a time":
485	// https://learn.microsoft.com/en-au/dotnet/api/system.io.binaryreader.readstring
486
487	// h/t https://stackoverflow.com/a/49780224
488	// h/t https://en.wikipedia.org/wiki/LEB128#Decode_signed_integer
489
490	// when parsing byte `n`, we want to shift it by `n*7` bits
491	let shifts = (0..=u32::MAX).step_by(7);
492
493	input
494		// take while the top bit is set, *and* the first byte to not have it set
495		.take_while_inclusive(|&byte| byte >= 0b1000_0000)
496		.zip(shifts)
497		.try_fold(0i32, |acc, (byte, shift)| {
498			i32::from(byte & 0b0111_1111) // mask off top bit
499				.checked_shl(shift)
500				.and_then(|value| acc.checked_add(value))
501				.ok_or_eyre("integer value too large: overflow")
502		})
503}
504
505/// Encodes a "seven bit integer" as read by
506/// [C#'s `BinaryReader`](https://learn.microsoft.com/en-au/dotnet/api/system.io.binaryreader.readstring).
507#[must_use]
508pub fn encode_seven_bit_int(value: i32) -> Vec<u8> {
509	let mut output = Vec::with_capacity(4);
510
511	#[allow(clippy::cast_sign_loss)]
512	let mut value = value as u32;
513
514	// mask off the top bit
515	let mask = 0b1000_0000;
516
517	while value >= mask {
518		#[allow(clippy::cast_possible_truncation)]
519		output.push((value | mask) as u8);
520		value >>= 7;
521	}
522
523	// final byte
524	#[allow(clippy::cast_possible_truncation)]
525	output.push(value as u8);
526
527	output
528}
529
530/// Encodes an `EntityMap` by calling [`Value::encode`] on each value.
531#[must_use]
532pub fn encode_entities(entities: &EntityMap) -> Vec<u8> {
533	let mut output = Vec::new();
534	for (key, value) in entities {
535		output.push(value.tag_byte());
536		let key = Value::String(key.clone()).encode();
537		output.extend(key);
538		output.extend(value.encode());
539	}
540	// null terminator
541	output.push(0);
542	output
543}