Skip to main content

vintage_schematics/entity/
mod.rs

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