luanti_protocol/
types.rs

1//! Luanti data types used inside of Commands / Packets.
2//!
3//! Derive macros `LuantiSerialize` and `LuantiDeserialize` are used to
4//! produce ser/deser methods for many of the structs below. The order of
5//! the fields inside the struct determines the order in which they are
6//! serialized/deserialized, so be careful modifying anything below.
7//! Their serialized representation must stay the same.
8//!
9//! NOTE: The derive macros currently do not work on structs with generic parameters.
10//!
11//! TODO(paradust): Having an assert!-like macro that generates Serialize/Deserialize
12//! errors instead of aborts may be helpful for cleaning this up.
13
14#![expect(
15    clippy::pub_underscore_fields,
16    clippy::used_underscore_binding,
17    reason = "required for de-/serialization macros"
18)]
19#![expect(
20    clippy::min_ident_chars,
21    reason = "those identifiers are well-known and clear from the context"
22)]
23
24mod active_object;
25mod arrays;
26mod binary;
27mod compressed;
28mod node_box;
29mod options;
30mod primitives;
31mod strings;
32mod tile;
33mod vectors;
34
35use crate::itos;
36use crate::wire::deser::Deserialize;
37use crate::wire::deser::DeserializeError;
38use crate::wire::deser::DeserializeResult;
39use crate::wire::deser::Deserializer;
40use crate::wire::packet::LATEST_PROTOCOL_VERSION;
41use crate::wire::packet::SER_FMT_HIGHEST_READ;
42use crate::wire::ser::Serialize;
43use crate::wire::ser::SerializeResult;
44use crate::wire::ser::Serializer;
45use crate::wire::ser::VecSerializer;
46use crate::wire::util::compress_zlib;
47use crate::wire::util::decompress_zlib;
48use crate::wire::util::deserialize_json_string_if_needed;
49use crate::wire::util::next_word;
50use crate::wire::util::serialize_json_string_if_needed;
51use crate::wire::util::skip_whitespace;
52use crate::wire::util::split_by_whitespace;
53use crate::wire::util::stoi;
54use crate::wire::util::zstd_compress;
55use crate::wire::util::zstd_decompress;
56pub use active_object::*;
57use anyhow::anyhow;
58use anyhow::bail;
59pub use arrays::*;
60pub use binary::*;
61pub use compressed::*;
62use luanti_protocol_derive::LuantiDeserialize;
63use luanti_protocol_derive::LuantiSerialize;
64pub use node_box::*;
65pub use options::*;
66pub use primitives::*;
67use std::marker::PhantomData;
68pub use strings::*;
69pub use tile::*;
70pub use vectors::*;
71
72pub type CommandId = u8;
73
74#[derive(Debug, Clone, Copy, PartialEq)]
75pub enum CommandDirection {
76    ToClient,
77    ToServer,
78}
79
80impl CommandDirection {
81    #[must_use]
82    pub fn for_send(remote_is_server: bool) -> Self {
83        if remote_is_server {
84            CommandDirection::ToServer
85        } else {
86            CommandDirection::ToClient
87        }
88    }
89
90    #[must_use]
91    pub fn for_receive(remote_is_server: bool) -> Self {
92        Self::for_send(remote_is_server).flip()
93    }
94
95    #[must_use]
96    pub fn flip(&self) -> Self {
97        match self {
98            CommandDirection::ToClient => CommandDirection::ToServer,
99            CommandDirection::ToServer => CommandDirection::ToClient,
100        }
101    }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct ProtocolContext {
106    pub dir: CommandDirection,
107    pub protocol_version: u16,
108    pub ser_fmt: u8,
109}
110
111impl ProtocolContext {
112    #[must_use]
113    pub fn latest_for_receive(remote_is_server: bool) -> Self {
114        Self {
115            dir: CommandDirection::for_receive(remote_is_server),
116            protocol_version: LATEST_PROTOCOL_VERSION,
117            ser_fmt: SER_FMT_HIGHEST_READ,
118        }
119    }
120
121    #[must_use]
122    pub fn latest_for_send(remote_is_server: bool) -> Self {
123        Self {
124            dir: CommandDirection::for_send(remote_is_server),
125            protocol_version: LATEST_PROTOCOL_VERSION,
126            ser_fmt: SER_FMT_HIGHEST_READ,
127        }
128    }
129}
130
131#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
132pub struct AddedObject {
133    pub id: u16,
134    pub typ: u8,
135    #[wrap(Wrapped32<GenericInitData>)]
136    pub init_data: GenericInitData,
137}
138
139#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
140pub struct MediaFileData {
141    pub name: String,
142    #[wrap(BinaryData32)]
143    pub data: Vec<u8>,
144}
145
146#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
147pub struct MediaAnnouncement {
148    pub name: String,
149    pub sha1_base64: String,
150}
151
152#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
153pub struct SkyColor {
154    pub day_sky: SColor,
155    pub day_horizon: SColor,
156    pub dawn_sky: SColor,
157    pub dawn_horizon: SColor,
158    pub night_sky: SColor,
159    pub night_horizon: SColor,
160    pub indoors: SColor,
161}
162
163#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
164pub struct SunParams {
165    pub visible: bool,
166    pub texture: String,
167    pub tonemap: String,
168    pub sunrise: String,
169    pub sunrise_visible: bool,
170    pub scale: f32,
171}
172
173#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
174pub struct MoonParams {
175    pub visible: bool,
176    pub texture: String,
177    pub tonemap: String,
178    pub scale: f32,
179}
180#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
181pub struct StarParams {
182    pub visible: bool,
183    pub count: u32,
184    pub starcolor: SColor,
185    pub scale: f32,
186    pub day_opacity: Option<f32>,
187}
188
189#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
190pub struct MinimapMode {
191    pub typ: u16,
192    pub label: String,
193    pub size: u16,
194    pub texture: String,
195    pub scale: u16,
196}
197
198#[derive(Debug, Clone, PartialEq)]
199pub struct PlayerPos {
200    pub position: v3f,     // serialized as v3s32, *100.0f
201    pub speed: v3f,        // serialized as v3s32, *100.0f
202    pub pitch: f32,        // serialized as s32, *100.0f
203    pub yaw: f32,          // serialized as s32, *100.0f
204    pub keys_pressed: u32, // bitset
205    pub fov: f32,          // serialized as u8, *80.0f
206    pub wanted_range: u8,
207
208    pub camera_inverted: bool,
209    pub movement_speed: f32,
210    pub movement_direction: f32,
211}
212
213impl Serialize for PlayerPos {
214    type Input = Self;
215    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
216        let s_position = (value.position * 100_f32).as_v3s32();
217        let s_speed = (value.speed * 100_f32).as_v3s32();
218        let s_pitch = (value.pitch * 100_f32).round() as s32;
219        let s_yaw = (value.yaw * 100_f32).round() as s32;
220        // scaled by 80, so that pi can fit into a u8
221        let s_fov = (value.fov * 80_f32).round() as u8;
222        let bits = u8::from(value.camera_inverted);
223
224        v3s32::serialize(&s_position, ser)?;
225        v3s32::serialize(&s_speed, ser)?;
226        i32::serialize(&s_pitch, ser)?;
227        i32::serialize(&s_yaw, ser)?;
228        u32::serialize(&value.keys_pressed, ser)?;
229        u8::serialize(&s_fov, ser)?;
230        u8::serialize(&value.wanted_range, ser)?;
231        u8::serialize(&bits, ser)?;
232        f32::serialize(&value.movement_speed, ser)?;
233        f32::serialize(&value.movement_direction, ser)?;
234        Ok(())
235    }
236}
237
238impl Deserialize for PlayerPos {
239    type Output = Self;
240    fn deserialize(deserializer: &mut Deserializer<'_>) -> DeserializeResult<Self> {
241        let s_position = v3s32::deserialize(deserializer)?;
242        let s_speed = v3s32::deserialize(deserializer)?;
243        let s_pitch = s32::deserialize(deserializer)?;
244        let s_yaw = s32::deserialize(deserializer)?;
245        let keys_pressed = u32::deserialize(deserializer)?;
246        let s_fov = u8::deserialize(deserializer)?;
247        let wanted_range = u8::deserialize(deserializer)?;
248
249        let bits = u8::deserialize(deserializer)?;
250
251        let movement_speed = f32::deserialize(deserializer)?;
252        let movement_direction = f32::deserialize(deserializer)?;
253
254        Ok(PlayerPos {
255            position: s_position.as_v3f() / 100_f32,
256            speed: s_speed.as_v3f() / 100_f32,
257            pitch: (s_pitch as f32) / 100_f32,
258            yaw: (s_yaw as f32) / 100_f32,
259            keys_pressed,
260            fov: f32::from(s_fov) / 80_f32,
261            wanted_range,
262            camera_inverted: bits & 0b0000_0001 != 0,
263            movement_speed,
264            movement_direction,
265        })
266    }
267}
268
269#[derive(Debug, Clone, PartialEq)]
270pub struct Pair<T1, T2>(PhantomData<(T1, T2)>);
271
272impl<T1: Serialize, T2: Serialize> Serialize for Pair<T1, T2>
273where
274    <T1 as Serialize>::Input: Sized,
275    <T2 as Serialize>::Input: Sized,
276{
277    type Input = (T1::Input, T2::Input);
278    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
279        <T1 as Serialize>::serialize(&value.0, ser)?;
280        <T2 as Serialize>::serialize(&value.1, ser)?;
281        Ok(())
282    }
283}
284
285impl<T1: Deserialize, T2: Deserialize> Deserialize for Pair<T1, T2> {
286    type Output = (T1::Output, T2::Output);
287    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self::Output> {
288        Ok((
289            <T1 as Deserialize>::deserialize(deser)?,
290            <T2 as Deserialize>::deserialize(deser)?,
291        ))
292    }
293}
294
295#[derive(Debug, Clone, PartialEq)]
296pub struct MinimapModeList {
297    pub mode: u16,
298    pub vec: Vec<MinimapMode>,
299}
300
301impl Serialize for MinimapModeList {
302    type Input = Self;
303    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
304        // The length of the list is a u16 which precedes `mode`,
305        // which makes the layout not fit into any usual pattern.
306        u16::serialize(&u16::try_from(value.vec.len())?, ser)?;
307        u16::serialize(&value.mode, ser)?;
308        for mode in &value.vec {
309            MinimapMode::serialize(mode, ser)?;
310        }
311        Ok(())
312    }
313}
314
315impl Deserialize for MinimapModeList {
316    type Output = Self;
317    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
318        let count = u16::deserialize(deser)?;
319        let mode = u16::deserialize(deser)?;
320        let mut vec: Vec<MinimapMode> = Vec::with_capacity(count as usize);
321        for _ in 0..count {
322            vec.push(MinimapMode::deserialize(deser)?);
323        }
324        Ok(MinimapModeList { mode, vec })
325    }
326}
327
328#[derive(Debug, Clone, PartialEq)]
329pub struct AuthMechsBitset {
330    pub legacy_password: bool,
331    pub srp: bool,
332    pub first_srp: bool,
333}
334
335impl Serialize for AuthMechsBitset {
336    type Input = Self;
337    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
338        let mut bits: u32 = 0;
339        if value.legacy_password {
340            bits |= 1;
341        }
342        if value.srp {
343            bits |= 2;
344        }
345        if value.first_srp {
346            bits |= 4;
347        }
348        u32::serialize(&bits, ser)
349    }
350}
351
352impl Deserialize for AuthMechsBitset {
353    type Output = Self;
354    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
355        let value = u32::deserialize(deser)?;
356        Ok(AuthMechsBitset {
357            legacy_password: (value & 1) != 0,
358            srp: (value & 2) != 0,
359            first_srp: (value & 4) != 0,
360        })
361    }
362}
363
364#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
365pub struct SimpleSoundSpec {
366    pub name: String,
367    pub gain: f32,
368    pub pitch: f32,
369    pub fade: f32,
370}
371
372#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
373pub enum DrawType {
374    Normal,
375    AirLike,
376    Liquid,
377    FlowingLiquid,
378    GlassLike,
379    AllFaces,
380    AllFacesOptional,
381    TorchLike,
382    SignLike,
383    PlantLike,
384    FenceLike,
385    RailLike,
386    NodeBox,
387    GlassLikeFramed,
388    FireLike,
389    GlassLikeFramedOptional,
390    Mesh,
391    PlantLikeRooted,
392}
393
394#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
395#[expect(clippy::struct_excessive_bools, reason = "this is mandated by the API")]
396pub struct ContentFeatures {
397    pub version: u8,
398    pub name: String,
399    #[wrap(Array16<Pair<String, s16>>)]
400    pub groups: Vec<(String, s16)>,
401    pub param_type: u8,
402    pub param_type_2: u8,
403    pub drawtype: DrawType,
404    pub mesh: String,
405    pub visual_scale: f32,
406    // this was an attempt to be tiledef length, but then they added an extra 6 tiledefs without fixing it
407    pub unused_six: u8,
408    #[wrap(FixedArray<6, TileDef>)]
409    pub tiledef: [TileDef; 6],
410    #[wrap(FixedArray<6, TileDef>)]
411    pub tiledef_overlay: [TileDef; 6],
412    #[wrap(Array8<TileDef>)]
413    pub tiledef_special: Vec<TileDef>,
414    pub alpha_for_legacy: u8,
415    pub red: u8,
416    pub green: u8,
417    pub blue: u8,
418    pub palette_name: String,
419    pub waving: u8,
420    pub connect_sides: u8,
421    #[wrap(Array16<u16>)]
422    pub connects_to_ids: Vec<u16>,
423    pub post_effect_color: SColor,
424    pub leveled: u8,
425    pub light_propagates: u8,
426    pub sunlight_propagates: u8,
427    pub light_source: u8,
428    pub is_ground_content: bool,
429    pub walkable: bool,
430    pub pointable: bool,
431    pub diggable: bool,
432    pub climbable: bool,
433    pub buildable_to: bool,
434    pub rightclickable: bool,
435    pub damage_per_second: u32,
436    pub liquid_type_bc: u8,
437    pub liquid_alternative_flowing: String,
438    pub liquid_alternative_source: String,
439    pub liquid_viscosity: u8,
440    pub liquid_renewable: bool,
441    pub liquid_range: u8,
442    pub drowning: u8,
443    pub floodable: bool,
444    pub node_box: NodeBox,
445    pub selection_box: NodeBox,
446    pub collision_box: NodeBox,
447    pub sound_footstep: SimpleSoundSpec,
448    pub sound_dig: SimpleSoundSpec,
449    pub sound_dug: SimpleSoundSpec,
450    pub legacy_facedir_simple: bool,
451    pub legacy_wallmounted: bool,
452    pub node_dig_prediction: Option<String>,
453    pub leveled_max: Option<u8>,
454    pub alpha: Option<AlphaMode>,
455    pub move_resistance: Option<u8>,
456    pub liquid_move_physics: Option<bool>,
457}
458
459#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
460pub enum AlphaMode {
461    Blend,
462    Clip,
463    Opaque,
464    LegacyCompat,
465}
466
467#[derive(Debug, Clone, PartialEq)]
468pub struct NodeDefManager {
469    pub content_features: Vec<(u16, ContentFeatures)>,
470}
471
472/// The way this structure is encoded is really unusual, in order to
473/// allow the `ContentFeatures` to be extended in the future without
474/// changing the encoding.
475impl Serialize for NodeDefManager {
476    type Input = Self;
477    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
478        // Version
479        u8::serialize(&1, ser)?;
480        let count: u16 = u16::try_from(value.content_features.len())?;
481        u16::serialize(&count, ser)?;
482        // The serialization of content_features is wrapped in a String32
483        // Write a marker so we can write the size later
484        let string32_wrapper = ser.write_marker(4)?;
485        for (index, features) in &value.content_features {
486            u16::serialize(index, ser)?;
487            // The contents of each feature is wrapped in a String16.
488            let string16_wrapper = ser.write_marker(2)?;
489            ContentFeatures::serialize(features, ser)?;
490            let len: u16 = u16::try_from(ser.marker_distance(&string16_wrapper))?;
491            ser.set_marker(string16_wrapper, &len.to_be_bytes()[..])?;
492        }
493        let len: u32 = u32::try_from(ser.marker_distance(&string32_wrapper))?;
494        ser.set_marker(string32_wrapper, &len.to_be_bytes()[..])?;
495        Ok(())
496    }
497}
498
499impl Deserialize for NodeDefManager {
500    type Output = Self;
501    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
502        let version = u8::deserialize(deser)?;
503        if version != 1 {
504            bail!(DeserializeError::InvalidValue(
505                "Bad NodeDefManager version".into(),
506            ));
507        }
508        let count: u16 = u16::deserialize(deser)?;
509        let string32_wrapper_len: u32 = u32::deserialize(deser)?;
510        // Shadow deser with a restricted deserializer
511        let mut deser = deser.slice(string32_wrapper_len as usize)?;
512        let mut content_features: Vec<(u16, ContentFeatures)> = Vec::with_capacity(count as usize);
513        for _ in 0..count {
514            let i = u16::deserialize(&mut deser)?;
515            let string16_wrapper_len: u16 = u16::deserialize(&mut deser)?;
516            let mut inner_deser = deser.slice(string16_wrapper_len as usize)?;
517            let features = ContentFeatures::deserialize(&mut inner_deser)?;
518            content_features.push((i, features));
519        }
520        Ok(Self { content_features })
521    }
522}
523
524// A "block" is 16x16x16 "nodes"
525const MAP_BLOCKSIZE: u16 = 16;
526
527// Number of nodes in a block
528const NODECOUNT: u16 = MAP_BLOCKSIZE * MAP_BLOCKSIZE * MAP_BLOCKSIZE;
529
530#[derive(Debug, Clone, PartialEq)]
531pub struct MapBlock {
532    pub is_underground: bool,
533    pub day_night_diff: bool,
534    pub generated: bool,
535    pub lighting_complete: Option<u16>,
536    pub nodes: MapNodesBulk,
537    pub node_metadata: NodeMetadataList, // m_node_metadata.serialize(os, version, disk);
538}
539
540impl Serialize for MapBlock {
541    /// `MapBlock` is a bit of a nightmare, because the compression algorithm
542    /// and where the compression is applied (to the whole struct, or to
543    /// parts of it) depends on the serialization format version.
544    ///
545    /// For now, only `ser_fmt` >= 28 is supported.
546    /// For ver 28, only the nodes and nodemeta are compressed using zlib.
547    /// For >= 29, the entire thing is compressed using zstd.
548    type Input = Self;
549    fn serialize<S: Serializer>(value: &Self::Input, serializer: &mut S) -> SerializeResult {
550        let ver = serializer.context().ser_fmt;
551        let mut tmp_ser = VecSerializer::new(serializer.context(), 0x8000);
552        let ser = &mut tmp_ser;
553        let header = MapBlockHeader {
554            is_underground: value.is_underground,
555            day_night_diff: value.day_night_diff,
556            generated: value.generated,
557            lighting_complete: value.lighting_complete,
558        };
559        MapBlockHeader::serialize(&header, ser)?;
560        if ver >= 29 {
561            MapNodesBulk::serialize(&value.nodes, ser)?;
562        } else {
563            // Serialize and compress using zlib
564            let mut inner = VecSerializer::new(ser.context(), 0x8000);
565            MapNodesBulk::serialize(&value.nodes, &mut inner)?;
566            let compressed = compress_zlib(&inner.take());
567            ser.write_bytes(&compressed)?;
568        }
569        if ver >= 29 {
570            NodeMetadataList::serialize(&value.node_metadata, ser)?;
571        } else {
572            // Serialize and compress using zlib
573            let mut inner = VecSerializer::new(ser.context(), 0x8000);
574            NodeMetadataList::serialize(&value.node_metadata, &mut inner)?;
575            let compressed = compress_zlib(&inner.take());
576            ser.write_bytes(&compressed)?;
577        }
578        if ver >= 29 {
579            // The whole thing is zstd compressed
580            let tmp = tmp_ser.take();
581            zstd_compress(&tmp, |chunk| serializer.write_bytes(chunk))?;
582        } else {
583            // Just write it directly
584            let tmp = tmp_ser.take();
585            serializer.write_bytes(&tmp)?;
586        }
587        Ok(())
588    }
589}
590
591///
592/// This is a helper for `MapBlock` ser/deser
593/// Not exposed publicly.
594#[derive(Debug)]
595struct MapBlockHeader {
596    pub is_underground: bool,
597    pub day_night_diff: bool,
598    pub generated: bool,
599    pub lighting_complete: Option<u16>,
600}
601
602impl Serialize for MapBlockHeader {
603    type Input = Self;
604    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
605        let mut flags: u8 = 0;
606        if value.is_underground {
607            flags |= 0x1;
608        }
609        if value.day_night_diff {
610            flags |= 0x2;
611        }
612        if !value.generated {
613            flags |= 0x8;
614        }
615        u8::serialize(&flags, ser)?;
616        if ser.context().ser_fmt >= 27 {
617            if let Some(lighting_complete) = value.lighting_complete {
618                u16::serialize(&lighting_complete, ser)?;
619            } else {
620                bail!("lighting_complete must be set for ver >= 27");
621            }
622        }
623        u8::serialize(&2, ser)?; // content_width == 2
624        u8::serialize(&2, ser)?; // params_width == 2
625        Ok(())
626    }
627}
628
629impl Deserialize for MapBlockHeader {
630    type Output = Self;
631    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
632        let flags = u8::deserialize(deser)?;
633        if flags != (flags & (0x1 | 0x2 | 0x8)) {
634            bail!(DeserializeError::InvalidValue(
635                "Invalid MapBlock flags".into(),
636            ));
637        }
638        #[expect(clippy::if_then_some_else_none, reason = "`?`-operator prohibits this")]
639        let lighting_complete = if deser.context().ser_fmt >= 27 {
640            Some(u16::deserialize(deser)?)
641        } else {
642            None
643        };
644        let content_width = u8::deserialize(deser)?;
645        let params_width = u8::deserialize(deser)?;
646        if content_width != 2 || params_width != 2 {
647            bail!(DeserializeError::InvalidValue(
648                "Corrupt MapBlock: content_width and params_width not both 2".into(),
649            ));
650        }
651        Ok(Self {
652            is_underground: (flags & 0x1) != 0,
653            day_night_diff: (flags & 0x2) != 0,
654            generated: (flags & 0x8) == 0,
655            lighting_complete,
656        })
657    }
658}
659
660impl Deserialize for MapBlock {
661    type Output = Self;
662    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
663        let ver = deser.context().ser_fmt;
664        if ver < 28 {
665            bail!("Unsupported ser fmt");
666        }
667        // TODO(paradust): I can't make the borrow checker happy with sharing
668        // code here, so for now the code has two different paths.
669        if ver >= 29 {
670            let mut tmp: Vec<u8> = Vec::new();
671            // Decompress to a temporary buffer
672            let bytes_taken = zstd_decompress(deser.peek_all(), |chunk| {
673                tmp.extend_from_slice(chunk);
674                Ok(())
675            })?;
676            deser.take(bytes_taken)?;
677            let deser = &mut Deserializer::new(deser.context(), &tmp);
678            let header = MapBlockHeader::deserialize(deser)?;
679            let nodes = MapNodesBulk::deserialize(deser)?;
680            let node_metadata = NodeMetadataList::deserialize(deser)?;
681            Ok(Self {
682                is_underground: header.is_underground,
683                day_night_diff: header.day_night_diff,
684                generated: header.generated,
685                lighting_complete: header.lighting_complete,
686                nodes,
687                node_metadata,
688            })
689        } else {
690            let header = MapBlockHeader::deserialize(deser)?;
691            let (consumed1, nodes_raw) = decompress_zlib(deser.peek_all())?;
692            deser.take(consumed1)?;
693            let nodes = {
694                let mut tmp = Deserializer::new(deser.context(), &nodes_raw);
695                MapNodesBulk::deserialize(&mut tmp)?
696            };
697            let (consumed2, metadata_raw) = decompress_zlib(deser.peek_all())?;
698            deser.take(consumed2)?;
699            let node_metadata = {
700                let mut tmp = Deserializer::new(deser.context(), &metadata_raw);
701                NodeMetadataList::deserialize(&mut tmp)?
702            };
703            Ok(Self {
704                is_underground: header.is_underground,
705                day_night_diff: header.day_night_diff,
706                generated: header.generated,
707                lighting_complete: header.lighting_complete,
708                nodes,
709                node_metadata,
710            })
711        }
712    }
713}
714
715/// This has a special serialization, presumably to make it compress better.
716/// Each param is stored in a separate array.
717#[derive(Debug, Clone, PartialEq)]
718pub struct MapNodesBulk {
719    pub nodes: [MapNode; NODECOUNT as usize],
720}
721
722impl Serialize for MapNodesBulk {
723    type Input = Self;
724    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
725        let nodecount = NODECOUNT as usize;
726        // Write all param0 first
727        ser.write(2 * nodecount, |buf| {
728            assert_eq!(buf.len(), 2 * nodecount, "size mismatch");
729            for index in 0..nodecount {
730                let bytes = value.nodes[index].param0.to_be_bytes();
731                buf[2 * index] = bytes[0];
732                buf[2 * index + 1] = bytes[1];
733            }
734        })?;
735        // Write all param1
736        ser.write(nodecount, |buf| {
737            assert_eq!(buf.len(), nodecount, "size mismatch");
738            #[expect(
739                clippy::needless_range_loop,
740                reason = "// TODO transform into iterator"
741            )]
742            for index in 0..nodecount {
743                buf[index] = value.nodes[index].param1;
744            }
745        })?;
746        // Write all param2
747        ser.write(nodecount, |buf| {
748            assert_eq!(buf.len(), nodecount, "size mismatch");
749            #[expect(
750                clippy::needless_range_loop,
751                reason = "// TODO transform into iterator"
752            )]
753            for i in 0..nodecount {
754                buf[i] = value.nodes[i].param2;
755            }
756        })?;
757        Ok(())
758    }
759}
760
761impl Deserialize for MapNodesBulk {
762    type Output = Self;
763    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
764        let nodecount = NODECOUNT as usize;
765        let data = deser.take(4 * nodecount)?;
766        let mut nodes: Vec<MapNode> = Vec::with_capacity(nodecount);
767        let param1_offset = 2 * nodecount;
768        let param2_offset = 3 * nodecount;
769        for i in 0..nodecount {
770            nodes.push(MapNode {
771                param0: u16::from_be_bytes(data[2 * i..2 * i + 2].try_into().unwrap()),
772                param1: data[param1_offset + i],
773                param2: data[param2_offset + i],
774            });
775        }
776        Ok(Self {
777            nodes: match nodes.try_into() {
778                Ok(value) => value,
779                Err(_) => bail!("Bug in MapNodesBulk"),
780            },
781        })
782    }
783}
784
785/// The default serialization is used for single nodes.
786/// But for transferring entire blocks, `MapNodeBulk` is used instead.
787#[derive(Debug, Default, Clone, Copy, PartialEq, LuantiSerialize, LuantiDeserialize)]
788pub struct MapNode {
789    pub param0: u16,
790    pub param1: u8,
791    pub param2: u8,
792}
793
794#[derive(Debug, Clone, PartialEq)]
795pub struct NodeMetadataList {
796    pub metadata: Vec<(BlockPos, NodeMetadata)>,
797}
798
799impl Serialize for NodeMetadataList {
800    type Input = Self;
801    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
802        if value.metadata.is_empty() {
803            u8::serialize(&0, ser)?; // version 0 indicates no data
804            return Ok(());
805        }
806        u8::serialize(&2, ser)?; // version == 2
807        <Array16<Pair<BlockPos, NodeMetadata>> as Serialize>::serialize(&value.metadata, ser)?;
808        Ok(())
809    }
810}
811
812impl Deserialize for NodeMetadataList {
813    type Output = Self;
814    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
815        let ver = u8::deserialize(deser)?;
816        if ver == 0 {
817            Ok(Self {
818                metadata: Vec::new(),
819            })
820        } else if ver == 2 {
821            Ok(Self {
822                metadata: <Array16<Pair<BlockPos, NodeMetadata>> as Deserialize>::deserialize(
823                    deser,
824                )?,
825            })
826        } else {
827            bail!(DeserializeError::InvalidValue(
828                "Invalid NodeMetadataList version".into(),
829            ))
830        }
831    }
832}
833
834#[derive(Debug, Clone, PartialEq)]
835pub struct AbsNodeMetadataList {
836    pub metadata: Vec<(AbsBlockPos, NodeMetadata)>,
837}
838
839impl Serialize for AbsNodeMetadataList {
840    type Input = Self;
841    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
842        if value.metadata.is_empty() {
843            u8::serialize(&0, ser)?; // version 0 indicates no data
844            return Ok(());
845        }
846        u8::serialize(&2, ser)?; // version == 2
847        <Array16<Pair<AbsBlockPos, NodeMetadata>> as Serialize>::serialize(&value.metadata, ser)?;
848        Ok(())
849    }
850}
851
852impl Deserialize for AbsNodeMetadataList {
853    type Output = Self;
854    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
855        let ver = u8::deserialize(deser)?;
856        if ver == 0 {
857            Ok(Self {
858                metadata: Vec::new(),
859            })
860        } else if ver == 2 {
861            Ok(Self {
862                metadata: <Array16<Pair<AbsBlockPos, NodeMetadata>> as Deserialize>::deserialize(
863                    deser,
864                )?,
865            })
866        } else {
867            bail!(DeserializeError::InvalidValue(
868                "Invalid AbsNodeMetadataList version".into(),
869            ))
870        }
871    }
872}
873
874#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
875pub struct AbsBlockPos {
876    pos: v3s16,
877}
878
879/// `BlockPos` addresses a node within a block
880/// It is equivalent to (16*z + y)*16 + x, where x,y,z are from 0 to 15.
881#[derive(Debug, Clone, PartialEq)]
882pub struct BlockPos {
883    pub raw: u16,
884}
885
886impl BlockPos {
887    #[must_use]
888    pub fn new(x: s16, y: s16, z: s16) -> Self {
889        let valid = 0..(MAP_BLOCKSIZE as s16);
890        assert!(
891            valid.contains(&x) && valid.contains(&y) && valid.contains(&z),
892            "//TODO add proper error message"
893        );
894        let x = x as u16;
895        let y = y as u16;
896        let z = z as u16;
897        Self {
898            raw: (MAP_BLOCKSIZE * z + y) * MAP_BLOCKSIZE + x,
899        }
900    }
901
902    #[must_use]
903    pub fn from_xyz(pos: v3s16) -> Self {
904        Self::new(pos.x, pos.y, pos.z)
905    }
906
907    #[must_use]
908    pub fn to_xyz(&self) -> v3s16 {
909        let x = self.raw % 16;
910        let y = (self.raw / 16) % 16;
911        let z = (self.raw / 256) % 16;
912        v3s16::new(x as i16, y as i16, z as i16)
913    }
914}
915
916impl Serialize for BlockPos {
917    type Input = Self;
918    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
919        u16::serialize(&value.raw, ser)?;
920        Ok(())
921    }
922}
923
924impl Deserialize for BlockPos {
925    type Output = Self;
926    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
927        let raw = u16::deserialize(deser)?;
928        if raw >= 4096 {
929            bail!(DeserializeError::InvalidValue("Invalid BlockPos".into(),))
930        }
931        Ok(Self { raw })
932    }
933}
934
935#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
936pub struct NodeMetadata {
937    #[wrap(Array32<StringVar>)]
938    pub stringvars: Vec<StringVar>,
939    pub inventory: Inventory,
940}
941
942#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
943pub struct StringVar {
944    pub name: String,
945    #[wrap(BinaryData32)]
946    pub value: Vec<u8>,
947    pub is_private: bool,
948}
949
950#[derive(Debug, Clone, PartialEq)]
951pub struct Inventory {
952    pub entries: Vec<InventoryEntry>,
953}
954
955#[derive(Debug, Clone, PartialEq)]
956pub enum InventoryEntry {
957    // Inventory lists to keep
958    KeepList(String),
959    // Inventory lists to add or update
960    Update(InventoryList),
961}
962
963/// Inventory is sent as a "almost" line-based text format.
964/// Unfortunately there's no way to simplify this code, it has to mirror
965/// the way Luanti does it exactly, because it is so arbitrary.
966impl Serialize for Inventory {
967    type Input = Self;
968    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
969        for entry in &value.entries {
970            match entry {
971                InventoryEntry::KeepList(list_name) => {
972                    // TODO(paradust): Performance. A format!-like macro that
973                    //                 writes directly to ser could be faster.
974                    ser.write_bytes(b"KeepList ")?;
975                    ser.write_bytes(list_name.as_bytes())?;
976                    ser.write_bytes(b"\n")?;
977                }
978                InventoryEntry::Update(list) => {
979                    // Takes care of the List header line
980                    InventoryList::serialize(list, ser)?;
981                }
982            }
983        }
984        ser.write_bytes(b"EndInventory\n")?;
985        Ok(())
986    }
987}
988
989impl Deserialize for Inventory {
990    type Output = Self;
991    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
992        let mut result = Self {
993            entries: Vec::new(),
994        };
995        while deser.has_remaining() {
996            // Peek the line, but don't take it yet.
997            let line = deser.peek_line()?;
998            let words = split_by_whitespace(line);
999            if words.is_empty() {
1000                deser.take_line()?;
1001                continue;
1002            }
1003            let name = words[0];
1004            if name == b"EndInventory" || name == b"End" {
1005                // Take the line
1006                deser.take_line()?;
1007                return Ok(result);
1008            } else if name == b"List" {
1009                // InventoryList will take the line
1010                result
1011                    .entries
1012                    .push(InventoryEntry::Update(InventoryList::deserialize(deser)?));
1013            } else if name == b"KeepList" {
1014                if words.len() < 2 {
1015                    bail!(DeserializeError::InvalidValue(
1016                        "KeepList missing name".into(),
1017                    ));
1018                }
1019                match std::str::from_utf8(words[1]) {
1020                    Ok(str) => result.entries.push(InventoryEntry::KeepList(str.into())),
1021                    Err(_) => {
1022                        bail!(DeserializeError::InvalidValue(
1023                            "KeepList name is invalid UTF8".into(),
1024                        ))
1025                    }
1026                }
1027                // Take the line
1028                deser.take_line()?;
1029            } else {
1030                // Anything else is supposed to be ignored. Gross.
1031                deser.take_line()?;
1032            }
1033        }
1034        // If we ran out before seeing the end marker, it's an error
1035        bail!(DeserializeError::Eof("Inventory::deserialize(_)".into()))
1036    }
1037}
1038
1039#[derive(Debug, Clone, PartialEq)]
1040pub struct InventoryList {
1041    pub name: String,
1042    pub width: u32,
1043    pub items: Vec<ItemStackUpdate>,
1044}
1045
1046#[derive(Debug, Clone, PartialEq)]
1047pub enum ItemStackUpdate {
1048    Empty,
1049    Keep, // this seems to not be used yet
1050    Item(ItemStack),
1051}
1052
1053impl Serialize for InventoryList {
1054    type Input = Self;
1055    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1056        // List <name> <size>
1057        ser.write_bytes(b"List ")?;
1058        ser.write_bytes(value.name.as_bytes())?;
1059        ser.write_bytes(b" ")?;
1060        ser.write_bytes(value.items.len().to_string().as_bytes())?;
1061        ser.write_bytes(b"\n")?;
1062
1063        // Width <width>
1064        ser.write_bytes(b"Width ")?;
1065        ser.write_bytes(value.width.to_string().as_bytes())?;
1066        ser.write_bytes(b"\n")?;
1067
1068        for item in &value.items {
1069            match item {
1070                ItemStackUpdate::Empty => ser.write_bytes(b"Empty\n")?,
1071                ItemStackUpdate::Keep => ser.write_bytes(b"Keep\n")?,
1072                ItemStackUpdate::Item(item_stack) => {
1073                    // Writes Item line
1074                    ItemStack::serialize(item_stack, ser)?;
1075                }
1076            }
1077        }
1078        ser.write_bytes(b"EndInventoryList\n")?;
1079        Ok(())
1080    }
1081}
1082
1083impl Deserialize for InventoryList {
1084    type Output = Self;
1085    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1086        // First line should be: List <name> <item_count>
1087        let line = deser.take_line()?;
1088        let words = split_by_whitespace(line);
1089        if words.len() != 3 || words[0] != b"List" {
1090            bail!(DeserializeError::InvalidValue("Broken List tag".into(),));
1091        }
1092        let list_name = std::str::from_utf8(words[1])?;
1093        let _count: u32 = stoi(words[2])?;
1094        let mut result = Self {
1095            name: list_name.into(),
1096            width: 0,
1097            items: Vec::new(),
1098        };
1099        while deser.has_remaining() {
1100            // Peek the line, but don't take it yet.
1101            let peeked_line = deser.peek_line()?;
1102            let peeked_words = split_by_whitespace(peeked_line);
1103            if peeked_words.is_empty() {
1104                deser.take_line()?;
1105                continue;
1106            }
1107            let name = peeked_words[0];
1108            if name == b"EndInventoryList" || name == b"end" {
1109                deser.take_line()?;
1110                return Ok(result);
1111            } else if name == b"Width" {
1112                if peeked_words.len() < 2 {
1113                    bail!(DeserializeError::InvalidValue("Width value missing".into(),));
1114                }
1115                result.width = stoi(peeked_words[1])?;
1116                deser.take_line()?;
1117            } else if name == b"Item" {
1118                // ItemStack takes the line
1119                result
1120                    .items
1121                    .push(ItemStackUpdate::Item(ItemStack::deserialize(deser)?));
1122            } else if name == b"Empty" {
1123                result.items.push(ItemStackUpdate::Empty);
1124                deser.take_line()?;
1125            } else if name == b"Keep" {
1126                result.items.push(ItemStackUpdate::Keep);
1127                deser.take_line()?;
1128            } else {
1129                // Ignore unrecognized lines
1130                deser.take_line()?;
1131            }
1132        }
1133        bail!(DeserializeError::Eof(
1134            "InventoryList::deserialize(_)".into()
1135        ))
1136    }
1137}
1138
1139// Custom deserialization, part of Inventory
1140#[derive(Debug, Clone, PartialEq)]
1141pub struct ItemStack {
1142    pub name: String,
1143    pub count: u16,
1144    pub wear: u16,
1145    pub metadata: ItemStackMetadata,
1146}
1147
1148impl Serialize for ItemStack {
1149    type Input = Self;
1150    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1151        // Item <name_json> [count] [wear] [metadata]
1152        ser.write_bytes(b"Item ")?;
1153        serialize_json_string_if_needed(value.name.as_bytes(), |chunk| ser.write_bytes(chunk))?;
1154
1155        let mut parts = 1;
1156        if !value.metadata.string_vars.is_empty() {
1157            parts = 4;
1158        } else if value.wear != 0 {
1159            parts = 3;
1160        } else if value.count != 1 {
1161            parts = 2;
1162        }
1163
1164        if parts >= 2 {
1165            ser.write_bytes(b" ")?;
1166            ser.write_bytes(value.count.to_string().as_bytes())?;
1167        }
1168        if parts >= 3 {
1169            ser.write_bytes(b" ")?;
1170            ser.write_bytes(value.wear.to_string().as_bytes())?;
1171        }
1172        if parts >= 4 {
1173            ser.write_bytes(b" ")?;
1174            ItemStackMetadata::serialize(&value.metadata, ser)?;
1175        }
1176        ser.write_bytes(b"\n")?;
1177        Ok(())
1178    }
1179}
1180
1181impl Deserialize for ItemStack {
1182    type Output = Self;
1183    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1184        // Item "name maybe escaped" [count] [wear] ["metadata escaped"]
1185        let line = deser.take_line()?;
1186        let err = DeserializeError::InvalidValue("Truncated Item line".into());
1187        let (first_word, line) = next_word(line).ok_or(err)?;
1188        if first_word != b"Item" {
1189            bail!(DeserializeError::InvalidValue("Invalid Item line".into(),));
1190        }
1191        let line = skip_whitespace(line);
1192        let (name, skip) = deserialize_json_string_if_needed(line)?;
1193        let line = skip_whitespace(&line[skip..]);
1194
1195        let mut result = Self {
1196            name: std::str::from_utf8(&name)?.into(),
1197            count: 1,
1198            wear: 0,
1199            metadata: ItemStackMetadata {
1200                string_vars: Vec::new(),
1201            },
1202        };
1203        if let Some((count_str, line)) = next_word(line) {
1204            result.count = stoi(count_str)?;
1205            if let Some((wear_str, line)) = next_word(line) {
1206                result.wear = stoi(wear_str)?;
1207                let line = skip_whitespace(line);
1208                if !line.is_empty() {
1209                    let mut tmp_deser = Deserializer::new(deser.context(), line);
1210                    result.metadata = ItemStackMetadata::deserialize(&mut tmp_deser)?;
1211                }
1212            }
1213        }
1214        Ok(result)
1215    }
1216}
1217
1218// Custom deserialization as json blob
1219#[derive(Debug, Clone, PartialEq)]
1220pub struct ItemStackMetadata {
1221    pub string_vars: Vec<(ByteString, ByteString)>,
1222}
1223
1224const DESERIALIZE_START: &[u8; 1] = b"\x01";
1225const DESERIALIZE_KV_DELIM: &[u8; 1] = b"\x02";
1226const DESERIALIZE_PAIR_DELIM: &[u8; 1] = b"\x03";
1227
1228impl Serialize for ItemStackMetadata {
1229    type Input = Self;
1230    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1231        let mut buf: Vec<u8> = Vec::new();
1232        buf.extend(DESERIALIZE_START);
1233        for (key, val) in &value.string_vars {
1234            if !key.is_empty() || !val.is_empty() {
1235                buf.extend(key.as_bytes());
1236                buf.extend(DESERIALIZE_KV_DELIM);
1237                buf.extend(val.as_bytes());
1238                buf.extend(DESERIALIZE_PAIR_DELIM);
1239            }
1240        }
1241        serialize_json_string_if_needed(&buf, |chunk| ser.write_bytes(chunk))?;
1242        Ok(())
1243    }
1244}
1245
1246impl Deserialize for ItemStackMetadata {
1247    type Output = Self;
1248    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1249        let (raw, count) = deserialize_json_string_if_needed(deser.peek_all())?;
1250        deser.take(count)?;
1251        let mut result = Self {
1252            string_vars: Vec::new(),
1253        };
1254        if raw.is_empty() {
1255            return Ok(result);
1256        }
1257        if raw[0] != DESERIALIZE_START[0] {
1258            bail!(DeserializeError::InvalidValue(
1259                "ItemStackMetadata bad start".into(),
1260            ));
1261        }
1262        let mut raw = &raw[1..];
1263        // This is odd, but matches the behavior of ItemStackMetadata::deSerialize
1264        while !raw.is_empty() {
1265            let kv_delim_pos = raw
1266                .iter()
1267                .position(|ch| *ch == DESERIALIZE_KV_DELIM[0])
1268                .unwrap_or(raw.len());
1269            let name = &raw[..kv_delim_pos];
1270            raw = &raw[kv_delim_pos..];
1271            if !raw.is_empty() {
1272                raw = &raw[1..];
1273            }
1274            let pair_delim_pos = raw
1275                .iter()
1276                .position(|ch| *ch == DESERIALIZE_PAIR_DELIM[0])
1277                .unwrap_or(raw.len());
1278            let var = &raw[..pair_delim_pos];
1279            raw = &raw[pair_delim_pos..];
1280            if !raw.is_empty() {
1281                raw = &raw[1..];
1282            }
1283            result.string_vars.push((name.into(), var.into()));
1284        }
1285        Ok(result)
1286    }
1287}
1288
1289#[derive(Debug, Default, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
1290pub struct RangedParameter<T: Serialize + Deserialize>
1291where
1292    T: Serialize<Input = T>,
1293    T: Deserialize<Output = T>,
1294{
1295    pub min: T,
1296    pub max: T,
1297    pub bias: f32,
1298}
1299
1300#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
1301pub struct Lighting {
1302    pub shadow_intensity: f32,
1303    pub saturation: f32,
1304    pub exposure: AutoExposure,
1305
1306    pub volumetric_light_strength: f32,
1307    pub shadow_tint: SColor,
1308    pub bloom_intensity: f32,
1309    pub bloom_strength_factor: f32,
1310    pub bloom_radius: f32,
1311}
1312
1313#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
1314pub struct AutoExposure {
1315    pub luminance_min: f32,
1316    pub luminance_max: f32,
1317    pub exposure_correction: f32,
1318    pub speed_dark_bright: f32,
1319    pub speed_bright_dark: f32,
1320    pub center_weight_power: f32,
1321}
1322
1323#[derive(Debug, Clone, PartialEq)]
1324pub enum HudSetParam {
1325    SetHotBarItemCount(s32),
1326    SetHotBarImage(String),
1327    SetHotBarSelectedImage(String),
1328}
1329
1330impl Serialize for HudSetParam {
1331    type Input = Self;
1332    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1333        #![allow(clippy::enum_glob_use, reason = "improves readability")]
1334        use HudSetParam::*;
1335        let param: u16 = match value {
1336            SetHotBarItemCount(_) => 1,
1337            SetHotBarImage(_) => 2,
1338            SetHotBarSelectedImage(_) => 3,
1339        };
1340        u16::serialize(&param, ser)?;
1341        match value {
1342            SetHotBarItemCount(value) => {
1343                // The value is wrapped in a a String16
1344                u16::serialize(&4, ser)?;
1345                s32::serialize(value, ser)?;
1346            }
1347            SetHotBarImage(value) => String::serialize(value, ser)?,
1348            SetHotBarSelectedImage(value) => String::serialize(value, ser)?,
1349        };
1350        Ok(())
1351    }
1352}
1353
1354impl Deserialize for HudSetParam {
1355    type Output = Self;
1356    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1357        #[allow(clippy::enum_glob_use, reason = "improves readability")]
1358        use HudSetParam::*;
1359        let param = u16::deserialize(deser)?;
1360        Ok(match param {
1361            1 => {
1362                let size = u16::deserialize(deser)?;
1363                if size != 4 {
1364                    bail!("Invalid size in SetHotBarItemCount: {}", size);
1365                }
1366                SetHotBarItemCount(s32::deserialize(deser)?)
1367            }
1368            2 => SetHotBarImage(String::deserialize(deser)?),
1369            3 => SetHotBarSelectedImage(String::deserialize(deser)?),
1370            _ => bail!("Invalid HudSetParam param: {}", param),
1371        })
1372    }
1373}
1374
1375#[derive(Debug, Clone, PartialEq)]
1376#[expect(
1377    clippy::struct_excessive_bools,
1378    reason = "// TODO rewrite using a crate for flags or bit-fields"
1379)]
1380pub struct HudFlags {
1381    pub hotbar_visible: bool,
1382    pub healthbar_visible: bool,
1383    pub crosshair_visible: bool,
1384    pub wielditem_visible: bool,
1385    pub breathbar_visible: bool,
1386    pub minimap_visible: bool,
1387    pub minimap_radar_visible: bool,
1388    pub basic_debug: bool,
1389    pub chat_visible: bool,
1390}
1391
1392impl HudFlags {
1393    #[must_use]
1394    pub fn to_u32(&self) -> u32 {
1395        #![expect(clippy::identity_op, reason = "for symmetry")]
1396        let mut flags: u32 = 0;
1397        flags |= u32::from(self.hotbar_visible) << 0;
1398        flags |= u32::from(self.healthbar_visible) << 1;
1399        flags |= u32::from(self.crosshair_visible) << 2;
1400        flags |= u32::from(self.wielditem_visible) << 3;
1401        flags |= u32::from(self.breathbar_visible) << 4;
1402        flags |= u32::from(self.minimap_visible) << 5;
1403        flags |= u32::from(self.minimap_radar_visible) << 6;
1404        flags |= u32::from(self.basic_debug) << 7;
1405        flags |= u32::from(self.chat_visible) << 8;
1406        flags
1407    }
1408
1409    #[must_use]
1410    pub fn from_u32(flags: u32) -> Self {
1411        Self {
1412            hotbar_visible: (flags & (1 << 0)) != 0,
1413            healthbar_visible: (flags & (1 << 1)) != 0,
1414            crosshair_visible: (flags & (1 << 2)) != 0,
1415            wielditem_visible: (flags & (1 << 3)) != 0,
1416            breathbar_visible: (flags & (1 << 4)) != 0,
1417            minimap_visible: (flags & (1 << 5)) != 0,
1418            minimap_radar_visible: (flags & (1 << 6)) != 0,
1419            basic_debug: (flags & (1 << 7)) != 0,
1420            chat_visible: (flags & (1 << 8)) != 0,
1421        }
1422    }
1423}
1424
1425impl Serialize for HudFlags {
1426    type Input = Self;
1427    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1428        let value = value.to_u32();
1429        u32::serialize(&value, ser)
1430    }
1431}
1432
1433impl Deserialize for HudFlags {
1434    type Output = Self;
1435    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1436        let value = u32::deserialize(deser)?;
1437        if (value & !0b1_1111_1111) != 0 {
1438            bail!("Invalid HudFlags: {}", value);
1439        }
1440        Ok(HudFlags::from_u32(value))
1441    }
1442}
1443
1444#[derive(Debug, Clone, PartialEq, LuantiSerialize, LuantiDeserialize)]
1445pub enum InteractAction {
1446    StartDigging,
1447    StopDigging,
1448    DiggingCompleted,
1449    Place,
1450    Use,
1451    Activate,
1452}
1453
1454#[derive(Debug, Clone, PartialEq)]
1455#[expect(
1456    variant_size_differences,
1457    reason = "// TODO review whether this is actually a problem"
1458)]
1459pub enum PointedThing {
1460    Nothing,
1461    Node {
1462        under_surface: v3s16,
1463        above_surface: v3s16,
1464    },
1465    Object {
1466        object_id: u16,
1467    },
1468}
1469
1470impl Serialize for PointedThing {
1471    type Input = Self;
1472    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1473        // version, always 0
1474        u8::serialize(&0, ser)?;
1475
1476        let typ: u8 = match value {
1477            PointedThing::Nothing => 0,
1478            PointedThing::Node { .. } => 1,
1479            PointedThing::Object { .. } => 2,
1480        };
1481        u8::serialize(&typ, ser)?;
1482
1483        match value {
1484            PointedThing::Nothing => (),
1485            PointedThing::Node {
1486                under_surface,
1487                above_surface,
1488            } => {
1489                v3s16::serialize(under_surface, ser)?;
1490                v3s16::serialize(above_surface, ser)?;
1491            }
1492            PointedThing::Object { object_id } => {
1493                u16::serialize(object_id, ser)?;
1494            }
1495        }
1496        Ok(())
1497    }
1498}
1499
1500impl Deserialize for PointedThing {
1501    type Output = Self;
1502    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1503        let ver = u8::deserialize(deser)?;
1504        if ver != 0 {
1505            bail!("Invalid PointedThing version: {}", ver);
1506        }
1507        let typ = u8::deserialize(deser)?;
1508        Ok(match typ {
1509            0 => PointedThing::Nothing,
1510            1 => PointedThing::Node {
1511                under_surface: v3s16::deserialize(deser)?,
1512                above_surface: v3s16::deserialize(deser)?,
1513            },
1514            2 => PointedThing::Object {
1515                object_id: u16::deserialize(deser)?,
1516            },
1517            _ => bail!("Invalid PointedThing type: {}", typ),
1518        })
1519    }
1520}
1521
1522#[derive(Debug, Clone, PartialEq)]
1523pub enum InventoryAction {
1524    Move {
1525        count: u16,
1526        from_inv: InventoryLocation,
1527        from_list: String,
1528        from_i: s16,
1529        to_inv: InventoryLocation,
1530        to_list: String,
1531        to_i: Option<s16>,
1532    },
1533    Craft {
1534        count: u16,
1535        craft_inv: InventoryLocation,
1536    },
1537    Drop {
1538        count: u16,
1539        from_inv: InventoryLocation,
1540        from_list: String,
1541        from_i: s16,
1542    },
1543}
1544
1545impl Serialize for InventoryAction {
1546    type Input = Self;
1547    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1548        match value {
1549            InventoryAction::Move {
1550                count,
1551                from_inv,
1552                from_list,
1553                from_i,
1554                to_inv,
1555                to_list,
1556                to_i,
1557            } => {
1558                if to_i.is_some() {
1559                    ser.write_bytes(b"Move ")?;
1560                } else {
1561                    ser.write_bytes(b"MoveSomewhere ")?;
1562                }
1563                ser.write_bytes(itos!(count))?;
1564                ser.write_bytes(b" ")?;
1565                InventoryLocation::serialize(from_inv, ser)?;
1566                ser.write_bytes(b" ")?;
1567                ser.write_bytes(from_list.as_bytes())?;
1568                ser.write_bytes(b" ")?;
1569                ser.write_bytes(itos!(from_i))?;
1570                ser.write_bytes(b" ")?;
1571                InventoryLocation::serialize(to_inv, ser)?;
1572                ser.write_bytes(b" ")?;
1573                ser.write_bytes(to_list.as_bytes())?;
1574                if let Some(to_i) = to_i {
1575                    ser.write_bytes(b" ")?;
1576                    ser.write_bytes(itos!(to_i))?;
1577                }
1578            }
1579            InventoryAction::Craft { count, craft_inv } => {
1580                ser.write_bytes(b"Craft ")?;
1581                ser.write_bytes(itos!(count))?;
1582                ser.write_bytes(b" ")?;
1583                InventoryLocation::serialize(craft_inv, ser)?;
1584                // This extra space is present in Luanti
1585                ser.write_bytes(b" ")?;
1586            }
1587            InventoryAction::Drop {
1588                count,
1589                from_inv,
1590                from_list,
1591                from_i,
1592            } => {
1593                ser.write_bytes(b"Drop ")?;
1594                ser.write_bytes(itos!(count))?;
1595                ser.write_bytes(b" ")?;
1596                InventoryLocation::serialize(from_inv, ser)?;
1597                ser.write_bytes(b" ")?;
1598                ser.write_bytes(from_list.as_bytes())?;
1599                ser.write_bytes(b" ")?;
1600                ser.write_bytes(itos!(from_i))?;
1601            }
1602        }
1603        Ok(())
1604    }
1605}
1606
1607impl Deserialize for InventoryAction {
1608    type Output = Self;
1609    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1610        let word = deser.take_word(true);
1611        if word == b"Move" || word == b"MoveSomewhere" {
1612            Ok(InventoryAction::Move {
1613                count: stoi(deser.take_word(true))?,
1614                from_inv: InventoryLocation::deserialize(deser)?,
1615                from_list: std::str::from_utf8(deser.take_word(true))?.to_owned(),
1616                from_i: stoi(deser.take_word(true))?,
1617                to_inv: InventoryLocation::deserialize(deser)?,
1618                to_list: std::str::from_utf8(deser.take_word(true))?.to_owned(),
1619                #[expect(clippy::if_then_some_else_none, reason = "`?`-operator prohibits this")]
1620                to_i: if word == b"Move" {
1621                    Some(stoi(deser.take_word(true))?)
1622                } else {
1623                    None
1624                },
1625            })
1626        } else if word == b"Drop" {
1627            Ok(InventoryAction::Drop {
1628                count: stoi(deser.take_word(true))?,
1629                from_inv: InventoryLocation::deserialize(deser)?,
1630                from_list: std::str::from_utf8(deser.take_word(true))?.to_owned(),
1631                from_i: stoi(deser.take_word(true))?,
1632            })
1633        } else if word == b"Craft" {
1634            Ok(InventoryAction::Craft {
1635                count: stoi(deser.take_word(true))?,
1636                craft_inv: InventoryLocation::deserialize(deser)?,
1637            })
1638        } else {
1639            bail!("Invalid InventoryAction kind");
1640        }
1641    }
1642}
1643
1644#[derive(Debug, Clone, PartialEq)]
1645pub enum InventoryLocation {
1646    Undefined,
1647    CurrentPlayer,
1648    Player { name: String },
1649    NodeMeta { pos: v3s16 },
1650    Detached { name: String },
1651}
1652
1653impl Serialize for InventoryLocation {
1654    type Input = Self;
1655    fn serialize<S: Serializer>(value: &Self::Input, ser: &mut S) -> SerializeResult {
1656        match value {
1657            InventoryLocation::Undefined => ser.write_bytes(b"undefined")?,
1658            InventoryLocation::CurrentPlayer => ser.write_bytes(b"current_player")?,
1659            InventoryLocation::Player { name } => {
1660                ser.write_bytes(b"player:")?;
1661                ser.write_bytes(name.as_bytes())?;
1662            }
1663            InventoryLocation::NodeMeta { pos } => {
1664                ser.write_bytes(format!("nodemeta:{},{},{}", pos.x, pos.y, pos.z).as_bytes())?;
1665            }
1666            InventoryLocation::Detached { name } => {
1667                ser.write_bytes(b"detached:")?;
1668                ser.write_bytes(name.as_bytes())?;
1669            }
1670        }
1671        Ok(())
1672    }
1673}
1674
1675impl Deserialize for InventoryLocation {
1676    type Output = Self;
1677    fn deserialize(deser: &mut Deserializer<'_>) -> DeserializeResult<Self> {
1678        let word = deser.take_word(true);
1679        if word == b"undefined" {
1680            Ok(InventoryLocation::Undefined)
1681        } else if word == b"current_player" {
1682            Ok(InventoryLocation::CurrentPlayer)
1683        } else if word.starts_with(b"player:") {
1684            Ok(InventoryLocation::Player {
1685                name: std::str::from_utf8(&word[7..])?.into(),
1686            })
1687        } else if word.starts_with(b"nodemeta:") {
1688            // TODO replace with strip_prefix
1689            let coords: Vec<&[u8]> = word[9..].split(|&ch| ch == b',').collect();
1690            if coords.len() != 3 {
1691                bail!("Corrupted nodemeta InventoryLocation");
1692            }
1693            let mut xyz = [0_i16; 3];
1694            for (i, &n) in coords.iter().enumerate() {
1695                xyz[i] = stoi(n)?;
1696            }
1697            let pos = v3s16::new(xyz[0], xyz[1], xyz[2]);
1698            Ok(InventoryLocation::NodeMeta { pos })
1699        } else if word.starts_with(b"detached:") {
1700            Ok(InventoryLocation::Detached {
1701                name: std::str::from_utf8(&word[9..])?.into(),
1702            })
1703        } else {
1704            Err(anyhow!("Unknown InventoryLocation: {:?}", word))
1705        }
1706    }
1707}