Skip to main content

steel_registry/data_components/components/
item_colors.rs

1//! Vanilla item color and map ID components.
2
3use std::io::{Cursor, Result, Write};
4
5use simdnbt::owned::NbtTag;
6use simdnbt::{FromNbtTag, ToNbtTag};
7use steel_utils::codec::VarInt;
8use steel_utils::hash::{ComponentHasher, HashComponent};
9use steel_utils::nbt::NbtNumeric as _;
10use steel_utils::serial::{ReadFrom, WriteTo};
11
12use super::rgb_color::decode_rgb_color;
13use crate::DyeColor;
14
15/// RGB color applied to dyeable items.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct DyedItemColor {
18    rgb: i32,
19}
20
21impl DyedItemColor {
22    pub const LEATHER_COLOR: i32 = -6_265_536;
23
24    #[must_use]
25    pub const fn new(rgb: i32) -> Self {
26        Self { rgb }
27    }
28
29    #[must_use]
30    pub const fn rgb(self) -> i32 {
31        self.rgb
32    }
33
34    /// Applies Vanilla's intensity-preserving dye blend.
35    #[must_use]
36    pub fn apply_dyes(current: Option<Self>, dyes: &[DyeColor]) -> Self {
37        let mut red_total = 0;
38        let mut green_total = 0;
39        let mut blue_total = 0;
40        let mut intensity_total = 0;
41        let mut color_count = 0;
42
43        if let Some(current) = current {
44            let (red, green, blue) = rgb_channels(current.rgb);
45            intensity_total += red.max(green).max(blue);
46            red_total += red;
47            green_total += green;
48            blue_total += blue;
49            color_count += 1;
50        }
51        for dye in dyes {
52            let (red, green, blue) = rgb_channels(dye.texture_diffuse_color());
53            intensity_total += red.max(green).max(blue);
54            red_total += red;
55            green_total += green;
56            blue_total += blue;
57            color_count += 1;
58        }
59
60        let mut red = red_total / color_count;
61        let mut green = green_total / color_count;
62        let mut blue = blue_total / color_count;
63        let average_intensity = intensity_total as f32 / color_count as f32;
64        let result_intensity = red.max(green).max(blue) as f32;
65        red = (red as f32 * average_intensity / result_intensity) as i32;
66        green = (green as f32 * average_intensity / result_intensity) as i32;
67        blue = (blue as f32 * average_intensity / result_intensity) as i32;
68        Self::new((red << 16) | (green << 8) | blue)
69    }
70}
71
72const fn rgb_channels(color: i32) -> (i32, i32, i32) {
73    ((color >> 16) & 255, (color >> 8) & 255, color & 255)
74}
75
76impl WriteTo for DyedItemColor {
77    fn write(&self, writer: &mut impl Write) -> Result<()> {
78        self.rgb.write(writer)
79    }
80}
81
82impl ReadFrom for DyedItemColor {
83    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
84        Ok(Self::new(i32::read(data)?))
85    }
86}
87
88impl ToNbtTag for DyedItemColor {
89    fn to_nbt_tag(self) -> NbtTag {
90        NbtTag::Int(self.rgb)
91    }
92}
93
94impl FromNbtTag for DyedItemColor {
95    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
96        decode_rgb_color(&tag.to_owned()).map(Self::new)
97    }
98}
99
100impl HashComponent for DyedItemColor {
101    fn hash_component(&self, hasher: &mut ComponentHasher) {
102        hasher.put_int(self.rgb);
103    }
104}
105
106/// Color used to tint a filled map item.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct MapItemColor {
109    rgb: i32,
110}
111
112impl MapItemColor {
113    pub const DEFAULT: Self = Self::new(4_603_950);
114
115    #[must_use]
116    pub const fn new(rgb: i32) -> Self {
117        Self { rgb }
118    }
119
120    #[must_use]
121    pub const fn rgb(self) -> i32 {
122        self.rgb
123    }
124}
125
126impl WriteTo for MapItemColor {
127    fn write(&self, writer: &mut impl Write) -> Result<()> {
128        self.rgb.write(writer)
129    }
130}
131
132impl ReadFrom for MapItemColor {
133    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
134        Ok(Self::new(i32::read(data)?))
135    }
136}
137
138impl ToNbtTag for MapItemColor {
139    fn to_nbt_tag(self) -> NbtTag {
140        NbtTag::Int(self.rgb)
141    }
142}
143
144impl FromNbtTag for MapItemColor {
145    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
146        tag.codec_i32().map(Self::new)
147    }
148}
149
150impl HashComponent for MapItemColor {
151    fn hash_component(&self, hasher: &mut ComponentHasher) {
152        hasher.put_int(self.rgb);
153    }
154}
155
156/// Numeric identifier for a map saved-data entry.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct MapId {
159    id: i32,
160}
161
162impl MapId {
163    #[must_use]
164    pub const fn new(id: i32) -> Self {
165        Self { id }
166    }
167
168    #[must_use]
169    pub const fn id(self) -> i32 {
170        self.id
171    }
172
173    #[must_use]
174    pub fn key(self) -> String {
175        format!("maps/{}", self.id)
176    }
177}
178
179impl WriteTo for MapId {
180    fn write(&self, writer: &mut impl Write) -> Result<()> {
181        VarInt(self.id).write(writer)
182    }
183}
184
185impl ReadFrom for MapId {
186    fn read(data: &mut Cursor<&[u8]>) -> Result<Self> {
187        Ok(Self::new(VarInt::read(data)?.0))
188    }
189}
190
191impl ToNbtTag for MapId {
192    fn to_nbt_tag(self) -> NbtTag {
193        NbtTag::Int(self.id)
194    }
195}
196
197impl FromNbtTag for MapId {
198    fn from_nbt_tag(tag: simdnbt::borrow::NbtTag<'_, '_>) -> Option<Self> {
199        tag.codec_i32().map(Self::new)
200    }
201}
202
203impl HashComponent for MapId {
204    fn hash_component(&self, hasher: &mut ComponentHasher) {
205        hasher.put_int(self.id);
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use std::io::Cursor;
212
213    use simdnbt::ToNbtTag as _;
214    use simdnbt::borrow::read_tag;
215    use simdnbt::owned::{NbtList, NbtTag};
216    use steel_utils::hash::HashComponent as _;
217    use steel_utils::serial::{ReadFrom as _, WriteTo as _};
218
219    use super::{DyedItemColor, MapId, MapItemColor};
220
221    fn parse<T: simdnbt::FromNbtTag>(tag: NbtTag) -> Option<T> {
222        let mut bytes = Vec::new();
223        tag.write(&mut bytes);
224        let borrowed = read_tag(&mut Cursor::new(bytes.as_slice())).ok()?;
225        T::from_nbt_tag(borrowed.as_tag())
226    }
227
228    #[test]
229    fn dyed_color_accepts_ints_and_rgb_vectors() {
230        assert_eq!(
231            parse(NbtTag::Short(0x1234)),
232            Some(DyedItemColor::new(0x1234))
233        );
234        assert_eq!(
235            parse(NbtTag::List(NbtList::Float(vec![1.0, 0.5, 0.0]))),
236            Some(DyedItemColor::new(0xffff_7f00_u32 as i32))
237        );
238        assert_eq!(
239            parse::<DyedItemColor>(NbtTag::List(NbtList::Float(vec![1.0]))),
240            None
241        );
242        assert_eq!(
243            DyedItemColor::new(0x123456).to_nbt_tag(),
244            NbtTag::Int(0x123456)
245        );
246    }
247
248    #[test]
249    fn raw_int_network_codecs_round_trip() {
250        for value in [DyedItemColor::new(-1), DyedItemColor::new(0x123456)] {
251            let mut encoded = Vec::new();
252            value.write(&mut encoded).expect("color should encode");
253            assert_eq!(
254                DyedItemColor::read(&mut Cursor::new(encoded.as_slice()))
255                    .expect("color should decode"),
256                value
257            );
258        }
259
260        let value = MapItemColor::DEFAULT;
261        let mut encoded = Vec::new();
262        value.write(&mut encoded).expect("map color should encode");
263        assert_eq!(
264            MapItemColor::read(&mut Cursor::new(encoded.as_slice()))
265                .expect("map color should decode"),
266            value
267        );
268    }
269
270    #[test]
271    fn map_id_uses_varint_network_and_int_persistence() {
272        let value = MapId::new(-17);
273        let mut encoded = Vec::new();
274        value.write(&mut encoded).expect("map ID should encode");
275        assert_eq!(
276            MapId::read(&mut Cursor::new(encoded.as_slice())).expect("map ID should decode"),
277            value
278        );
279        assert_eq!(parse(NbtTag::Long(42)), Some(MapId::new(42)));
280        assert_eq!(value.to_nbt_tag(), NbtTag::Int(-17));
281        assert_eq!(MapId::new(42).key(), "maps/42");
282    }
283
284    #[test]
285    fn persistent_hashes_use_int_codec_shape() {
286        for (actual, expected) in [
287            (
288                DyedItemColor::new(0x123456).compute_hash(),
289                NbtTag::Int(0x123456).compute_hash(),
290            ),
291            (
292                MapItemColor::DEFAULT.compute_hash(),
293                NbtTag::Int(MapItemColor::DEFAULT.rgb()).compute_hash(),
294            ),
295            (MapId::new(7).compute_hash(), NbtTag::Int(7).compute_hash()),
296        ] {
297            assert_eq!(actual, expected);
298        }
299    }
300}