Skip to main content

steel_protocol/packets/game/
c_set_equipment.rs

1//! Clientbound set equipment packet.
2
3use std::io::{Error, ErrorKind, Result, Write};
4
5use steel_macros::ClientPacket;
6use steel_registry::{
7    equipment::EquipmentSlot, item_stack::ItemStack, packets::play::C_SET_EQUIPMENT,
8};
9use steel_utils::{codec::VarInt, serial::WriteTo};
10
11const CONTINUE_MASK: u8 = 0x80;
12
13const fn vanilla_equipment_slot_id(slot: EquipmentSlot) -> u8 {
14    match slot {
15        EquipmentSlot::MainHand => 0,
16        EquipmentSlot::OffHand => 1,
17        EquipmentSlot::Feet => 2,
18        EquipmentSlot::Legs => 3,
19        EquipmentSlot::Chest => 4,
20        EquipmentSlot::Head => 5,
21        EquipmentSlot::Body => 6,
22        EquipmentSlot::Saddle => 7,
23    }
24}
25
26const fn equipment_slot_packet_id(slot: EquipmentSlot, has_next: bool) -> u8 {
27    if has_next {
28        vanilla_equipment_slot_id(slot) | CONTINUE_MASK
29    } else {
30        vanilla_equipment_slot_id(slot)
31    }
32}
33
34/// One equipment slot update.
35#[derive(Clone, Debug, PartialEq)]
36pub struct EquipmentSlotItem {
37    /// Slot being updated.
38    pub slot: EquipmentSlot,
39    /// New item stack for the slot. Empty stacks clear the slot on the client.
40    pub item_stack: ItemStack,
41}
42
43/// Updates one or more equipment slots for an entity.
44#[derive(ClientPacket, Clone, Debug)]
45#[packet_id(Play = C_SET_EQUIPMENT)]
46pub struct CSetEquipment {
47    /// Entity id whose equipment changed.
48    pub entity_id: i32,
49    /// Slot updates. Vanilla requires at least one entry when this packet is sent.
50    pub slots: Vec<EquipmentSlotItem>,
51}
52
53impl CSetEquipment {
54    /// Creates a new equipment update packet.
55    #[must_use]
56    pub const fn new(entity_id: i32, slots: Vec<EquipmentSlotItem>) -> Self {
57        Self { entity_id, slots }
58    }
59}
60
61impl WriteTo for CSetEquipment {
62    fn write(&self, writer: &mut impl Write) -> Result<()> {
63        if self.slots.is_empty() {
64            return Err(Error::new(
65                ErrorKind::InvalidInput,
66                "CSetEquipment requires at least one slot",
67            ));
68        }
69        VarInt(self.entity_id).write(writer)?;
70        let last_index = self.slots.len().saturating_sub(1);
71        for (index, slot_item) in self.slots.iter().enumerate() {
72            writer.write_all(&[equipment_slot_packet_id(
73                slot_item.slot,
74                index != last_index,
75            )])?;
76            slot_item.item_stack.write(writer)?;
77        }
78        Ok(())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn equipment_packet_uses_vanilla_continue_bit() {
88        let packet = CSetEquipment::new(
89            42,
90            vec![
91                EquipmentSlotItem {
92                    slot: EquipmentSlot::MainHand,
93                    item_stack: ItemStack::empty(),
94                },
95                EquipmentSlotItem {
96                    slot: EquipmentSlot::Head,
97                    item_stack: ItemStack::empty(),
98                },
99            ],
100        );
101        let mut bytes = Vec::new();
102
103        packet.write(&mut bytes).expect("packet should encode");
104
105        assert_eq!(bytes, vec![42, 0x80, 0, 5, 0]);
106    }
107
108    #[test]
109    fn equipment_packet_rejects_empty_slot_updates() {
110        let packet = CSetEquipment::new(42, Vec::new());
111
112        let error = packet.write(&mut Vec::new()).unwrap_err();
113
114        assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
115    }
116}