Skip to main content

steel_utils/text/
mod.rs

1//! This module contains everything related to text components.
2mod nbt;
3
4pub use nbt::command_nbt_component;
5
6use crate::{
7    hash::{ComponentHasher, HashComponent, HashEntry, sort_map_entries},
8    serial::ReadFrom,
9    translations_registry::TRANSLATIONS,
10};
11use simdnbt::owned::read_tag;
12use std::io::{self, Cursor};
13use text_components::{
14    TextComponent,
15    content::{Content, NbtSource, Object, PlayerModel, Resolvable},
16    custom::{CustomData, Payload},
17    format::Format,
18    interactivity::{ClickEvent, Dialog, HoverEvent},
19    resolving::TextResolutor,
20};
21
22/// A [`TextResolutor`] for the console
23pub struct DisplayResolutor;
24impl TextResolutor for DisplayResolutor {
25    fn resolve_content(&self, resolvable: &Resolvable) -> TextComponent {
26        TextComponent {
27            content: Content::Resolvable(resolvable.clone()),
28            ..Default::default()
29        }
30    }
31
32    fn resolve_custom(&self, _data: &CustomData) -> Option<TextComponent> {
33        None
34    }
35
36    fn translate(&self, key: &str) -> Option<String> {
37        TRANSLATIONS.get(key).map(ToString::to_string)
38    }
39}
40
41impl ReadFrom for TextComponent {
42    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
43        // ComponentSerialization.STREAM_CODEC writes one unnamed NBT tag.
44        let nbt_tag =
45            read_tag(data).map_err(|e| io::Error::other(format!("Failed to read NBT: {e:?}")))?;
46
47        Self::from_nbt(&nbt_tag)
48            .ok_or_else(|| io::Error::other("Failed to parse TextComponent from NBT"))
49    }
50}
51
52impl HashComponent for TextComponent {
53    fn hash_component(&self, hasher: &mut ComponentHasher) {
54        // Minecraft's CODEC for Component uses an Either:
55        // - If the component is plain text only (no siblings, no style), encode as just a string
56        // - Otherwise, encode as a full map structure
57        //
58        // This matches ComponentSerialization.createCodec's tryCollapseToString logic
59        if let Content::Text { text } = &self.content
60            && self.format.is_none()
61            && self.interactions.is_none()
62            && self.children.is_empty()
63        {
64            hasher.put_string(text);
65            return;
66        }
67        // Complex component - hash as a map structure
68        hash_component_as_map(self, hasher);
69    }
70}
71
72/// Hash this component as a map structure (for non-collapsible components).
73fn hash_component_as_map(component: &TextComponent, hasher: &mut ComponentHasher) {
74    // Collect all map entries with their key and value hashes for sorting
75    let mut entries: Vec<HashEntry> = Vec::new();
76
77    // Hash content
78    hash_content_fields(&component.content, &mut entries);
79
80    // Hash style fields
81    hash_format_fields(&component.format, &mut entries);
82
83    if let Some(insertion) = &component.interactions.insertion {
84        let mut key_hasher = ComponentHasher::new();
85        key_hasher.put_string("insertion");
86        let mut value_hasher = ComponentHasher::new();
87        value_hasher.put_string(insertion);
88        entries.push(HashEntry::new(key_hasher, value_hasher));
89    }
90
91    if let Some(hover_event) = &component.interactions.hover {
92        let mut key_hasher = ComponentHasher::new();
93        key_hasher.put_string("hover_event");
94        let mut value_hasher = ComponentHasher::new();
95        hash_hover_fields(hover_event, &mut value_hasher);
96        entries.push(HashEntry::new(key_hasher, value_hasher));
97    }
98
99    if let Some(click_event) = &component.interactions.click {
100        let mut key_hasher = ComponentHasher::new();
101        key_hasher.put_string("click_event");
102        let mut value_hasher = ComponentHasher::new();
103        hash_click_fields(click_event, &mut value_hasher);
104        entries.push(HashEntry::new(key_hasher, value_hasher));
105    }
106
107    // Hash extra (siblings)
108    if !component.children.is_empty() {
109        let mut key_hasher = ComponentHasher::new();
110        key_hasher.put_string("extra");
111        let mut value_hasher = ComponentHasher::new();
112        value_hasher.start_list();
113        for child in &component.children {
114            value_hasher.put_component_hash(child);
115        }
116        value_hasher.end_list();
117        entries.push(HashEntry::new(key_hasher, value_hasher));
118    }
119
120    // Sort entries by key hash, then value hash (Minecraft's map ordering)
121    sort_map_entries(&mut entries);
122
123    // Write the sorted map
124    // Important: Vanilla writes the 4-byte hash values, NOT the original encoded bytes!
125    hasher.start_map();
126    for entry in entries {
127        hasher.put_raw_bytes(&entry.key_bytes);
128        hasher.put_raw_bytes(&entry.value_bytes);
129    }
130    hasher.end_map();
131}
132
133#[expect(
134    clippy::too_many_lines,
135    reason = "each Content variant requires distinct hashing logic; splitting would hurt readability"
136)]
137fn hash_content_fields(content: &Content, entries: &mut Vec<HashEntry>) {
138    match content {
139        Content::Text { text } => {
140            let mut key_hasher = ComponentHasher::new();
141            key_hasher.put_string("text");
142            let mut value_hasher = ComponentHasher::new();
143            value_hasher.put_string(text);
144            entries.push(HashEntry::new(key_hasher, value_hasher));
145        }
146        Content::Translate(message) => {
147            // "translate" field
148            {
149                let mut key_hasher = ComponentHasher::new();
150                key_hasher.put_string("translate");
151                let mut value_hasher = ComponentHasher::new();
152                value_hasher.put_string(&message.key);
153                entries.push(HashEntry::new(key_hasher, value_hasher));
154            }
155            // "fallback" field (optional)
156            if let Some(fallback) = &message.fallback {
157                let mut key_hasher = ComponentHasher::new();
158                key_hasher.put_string("fallback");
159                let mut value_hasher = ComponentHasher::new();
160                value_hasher.put_string(fallback);
161                entries.push(HashEntry::new(key_hasher, value_hasher));
162            }
163            // "with" field (optional args list)
164            if let Some(args) = &message.args
165                && !args.is_empty()
166            {
167                let mut key_hasher = ComponentHasher::new();
168                key_hasher.put_string("with");
169                let mut value_hasher = ComponentHasher::new();
170                value_hasher.start_list();
171                for arg in args {
172                    value_hasher.put_component_hash(arg);
173                }
174                value_hasher.end_list();
175                entries.push(HashEntry::new(key_hasher, value_hasher));
176            }
177        }
178        Content::Keybind { keybind } => {
179            let mut key_hasher = ComponentHasher::new();
180            key_hasher.put_string("keybind");
181            let mut value_hasher = ComponentHasher::new();
182            value_hasher.put_string(keybind);
183            entries.push(HashEntry::new(key_hasher, value_hasher));
184        }
185        Content::Object(Object::Atlas {
186            atlas,
187            sprite,
188            fallback,
189        }) => {
190            {
191                let mut key_hasher = ComponentHasher::new();
192                key_hasher.put_string("sprite");
193                let mut value_hasher = ComponentHasher::new();
194                value_hasher.put_string(sprite);
195                entries.push(HashEntry::new(key_hasher, value_hasher));
196            }
197            if atlas != "minecraft:blocks" {
198                let mut key_hasher = ComponentHasher::new();
199                key_hasher.put_string("atlas");
200                let mut value_hasher = ComponentHasher::new();
201                value_hasher.put_string(atlas);
202                entries.push(HashEntry::new(key_hasher, value_hasher));
203            }
204            if let Some(fallback) = fallback {
205                let mut key_hasher = ComponentHasher::new();
206                key_hasher.put_string("fallback");
207                let mut value_hasher = ComponentHasher::new();
208                fallback.hash_component(&mut value_hasher);
209                entries.push(HashEntry::new(key_hasher, value_hasher));
210            }
211        }
212        Content::Object(Object::Player {
213            player,
214            hat,
215            fallback,
216        }) => {
217            {
218                let mut inner_entries: Vec<HashEntry> = Vec::new();
219                if let Some(id) = &player.id {
220                    let mut key_hasher = ComponentHasher::new();
221                    key_hasher.put_string("id");
222                    let mut value_hasher = ComponentHasher::new();
223                    value_hasher.put_int_array(id);
224                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
225                }
226                if let Some(name) = &player.name {
227                    let mut key_hasher = ComponentHasher::new();
228                    key_hasher.put_string("name");
229                    let mut value_hasher = ComponentHasher::new();
230                    value_hasher.put_string(name);
231                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
232                }
233                if let Some(texture) = &player.texture {
234                    let mut key_hasher = ComponentHasher::new();
235                    key_hasher.put_string("texture");
236                    let mut value_hasher = ComponentHasher::new();
237                    value_hasher.put_string(texture);
238                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
239                }
240                if let Some(cape) = &player.cape {
241                    let mut key_hasher = ComponentHasher::new();
242                    key_hasher.put_string("cape");
243                    let mut value_hasher = ComponentHasher::new();
244                    value_hasher.put_string(cape);
245                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
246                }
247                if let Some(elytra) = &player.elytra {
248                    let mut key_hasher = ComponentHasher::new();
249                    key_hasher.put_string("elytra");
250                    let mut value_hasher = ComponentHasher::new();
251                    value_hasher.put_string(elytra);
252                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
253                }
254                if let Some(model) = player.model {
255                    let mut key_hasher = ComponentHasher::new();
256                    key_hasher.put_string("model");
257                    let mut value_hasher = ComponentHasher::new();
258                    value_hasher.put_string(match model {
259                        PlayerModel::Slim => "slim",
260                        PlayerModel::Wide => "wide",
261                    });
262                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
263                }
264                if !player.properties.is_empty() {
265                    let mut key_hasher = ComponentHasher::new();
266                    key_hasher.put_string("properties");
267                    let mut value_hasher = ComponentHasher::new();
268                    value_hasher.start_list();
269                    for property in &player.properties {
270                        let mut entries: Vec<HashEntry> = Vec::new();
271                        {
272                            let mut key_hasher = ComponentHasher::new();
273                            key_hasher.put_string("name");
274                            let mut value_hasher = ComponentHasher::new();
275                            value_hasher.put_string(&property.name);
276                            entries.push(HashEntry::new(key_hasher, value_hasher));
277                        }
278                        {
279                            let mut key_hasher = ComponentHasher::new();
280                            key_hasher.put_string("value");
281                            let mut value_hasher = ComponentHasher::new();
282                            value_hasher.put_string(&property.value);
283                            entries.push(HashEntry::new(key_hasher, value_hasher));
284                        }
285                        if let Some(signature) = &property.signature {
286                            let mut key_hasher = ComponentHasher::new();
287                            key_hasher.put_string("signature");
288                            let mut value_hasher = ComponentHasher::new();
289                            value_hasher.put_string(signature);
290                            entries.push(HashEntry::new(key_hasher, value_hasher));
291                        }
292
293                        // Sort entries by key hash, then value hash (Minecraft's map ordering)
294                        sort_map_entries(&mut entries);
295                        let mut hasher = ComponentHasher::new();
296                        hasher.start_map();
297                        for entry in entries {
298                            hasher.put_raw_bytes(&entry.key_bytes);
299                            hasher.put_raw_bytes(&entry.value_bytes);
300                        }
301                        hasher.end_map();
302                        // List elements are hashes, not full encoded bytes
303                        let property_hash = hasher.finish();
304                        value_hasher.put_raw_bytes(&property_hash.to_le_bytes());
305                    }
306                    value_hasher.end_list();
307                    inner_entries.push(HashEntry::new(key_hasher, value_hasher));
308                }
309                // Sort entries by key hash, then value hash (Minecraft's map ordering)
310                sort_map_entries(&mut inner_entries);
311
312                // Write the sorted map
313                let mut key_hasher = ComponentHasher::new();
314                key_hasher.put_string("player");
315                let mut value_hasher = ComponentHasher::new();
316                value_hasher.start_map();
317                for entry in inner_entries {
318                    value_hasher.put_raw_bytes(&entry.key_bytes);
319                    value_hasher.put_raw_bytes(&entry.value_bytes);
320                }
321                value_hasher.end_map();
322                entries.push(HashEntry::new(key_hasher, value_hasher));
323            }
324            if !hat {
325                let mut key_hasher = ComponentHasher::new();
326                key_hasher.put_string("hat");
327                let mut value_hasher = ComponentHasher::new();
328                value_hasher.put_bool(*hat);
329                entries.push(HashEntry::new(key_hasher, value_hasher));
330            }
331            if let Some(fallback) = fallback {
332                let mut key_hasher = ComponentHasher::new();
333                key_hasher.put_string("fallback");
334                let mut value_hasher = ComponentHasher::new();
335                fallback.hash_component(&mut value_hasher);
336                entries.push(HashEntry::new(key_hasher, value_hasher));
337            }
338        }
339        Content::Resolvable(Resolvable::Entity {
340            selector,
341            separator,
342        }) => {
343            // "selector" field
344            {
345                let mut key_hasher = ComponentHasher::new();
346                key_hasher.put_string("selector");
347                let mut value_hasher = ComponentHasher::new();
348                value_hasher.put_string(selector);
349                entries.push(HashEntry::new(key_hasher, value_hasher));
350            }
351            // "separator" field (optional)
352            if let Some(separator) = separator {
353                let mut key_hasher = ComponentHasher::new();
354                key_hasher.put_string("separator");
355                let mut value_hasher = ComponentHasher::new();
356                separator.hash_component(&mut value_hasher);
357                entries.push(HashEntry::new(key_hasher, value_hasher));
358            }
359        }
360        Content::Resolvable(Resolvable::Scoreboard {
361            selector,
362            objective,
363        }) => {
364            // "score" object with "name" and "objective" fields
365            let mut inner_entries: Vec<HashEntry> = Vec::new();
366            {
367                let mut key_hasher = ComponentHasher::new();
368                key_hasher.put_string("name");
369                let mut value_hasher = ComponentHasher::new();
370                value_hasher.put_string(selector);
371                inner_entries.push(HashEntry::new(key_hasher, value_hasher));
372            }
373            {
374                let mut key_hasher = ComponentHasher::new();
375                key_hasher.put_string("objective");
376                let mut value_hasher = ComponentHasher::new();
377                value_hasher.put_string(objective);
378                inner_entries.push(HashEntry::new(key_hasher, value_hasher));
379            }
380            // Sort entries by key hash, then value hash (Minecraft's map ordering)
381            sort_map_entries(&mut inner_entries);
382
383            // Write the sorted map under "score" key
384            let mut key_hasher = ComponentHasher::new();
385            key_hasher.put_string("score");
386            let mut value_hasher = ComponentHasher::new();
387            value_hasher.start_map();
388            for entry in inner_entries {
389                value_hasher.put_raw_bytes(&entry.key_bytes);
390                value_hasher.put_raw_bytes(&entry.value_bytes);
391            }
392            value_hasher.end_map();
393            entries.push(HashEntry::new(key_hasher, value_hasher));
394        }
395        Content::Resolvable(Resolvable::NBT {
396            path,
397            interpret,
398            plain,
399            separator,
400            source,
401        }) => {
402            // "nbt" field
403            {
404                let mut key_hasher = ComponentHasher::new();
405                key_hasher.put_string("nbt");
406                let mut value_hasher = ComponentHasher::new();
407                value_hasher.put_string(path);
408                entries.push(HashEntry::new(key_hasher, value_hasher));
409            }
410            // "interpret" field (optional, defaults to false)
411            if *interpret {
412                let mut key_hasher = ComponentHasher::new();
413                key_hasher.put_string("interpret");
414                let mut value_hasher = ComponentHasher::new();
415                value_hasher.put_bool(true);
416                entries.push(HashEntry::new(key_hasher, value_hasher));
417            }
418            // "plain" field (optional, defaults to false)
419            if *plain {
420                let mut key_hasher = ComponentHasher::new();
421                key_hasher.put_string("plain");
422                let mut value_hasher = ComponentHasher::new();
423                value_hasher.put_bool(true);
424                entries.push(HashEntry::new(key_hasher, value_hasher));
425            }
426            // "separator" field (optional)
427            if let Some(separator) = separator {
428                let mut key_hasher = ComponentHasher::new();
429                key_hasher.put_string("separator");
430                let mut value_hasher = ComponentHasher::new();
431                separator.hash_component(&mut value_hasher);
432                entries.push(HashEntry::new(key_hasher, value_hasher));
433            }
434            // Source field (entity, block, or storage)
435            match source {
436                NbtSource::Entity(selector) => {
437                    let mut key_hasher = ComponentHasher::new();
438                    key_hasher.put_string("entity");
439                    let mut value_hasher = ComponentHasher::new();
440                    value_hasher.put_string(selector);
441                    entries.push(HashEntry::new(key_hasher, value_hasher));
442                }
443                NbtSource::Block(pos) => {
444                    let mut key_hasher = ComponentHasher::new();
445                    key_hasher.put_string("block");
446                    let mut value_hasher = ComponentHasher::new();
447                    value_hasher.put_string(pos);
448                    entries.push(HashEntry::new(key_hasher, value_hasher));
449                }
450                NbtSource::Storage(id) => {
451                    let mut key_hasher = ComponentHasher::new();
452                    key_hasher.put_string("storage");
453                    let mut value_hasher = ComponentHasher::new();
454                    value_hasher.put_string(id);
455                    entries.push(HashEntry::new(key_hasher, value_hasher));
456                }
457            }
458        }
459        Content::Custom(_custom_data) => {
460            // Custom data components are resolved at runtime and should not appear
461            // in hashing for network protocol. If they do appear, we treat them
462            // as an empty text component.
463            let mut key_hasher = ComponentHasher::new();
464            key_hasher.put_string("text");
465            let mut value_hasher = ComponentHasher::new();
466            value_hasher.put_string("");
467            entries.push(HashEntry::new(key_hasher, value_hasher));
468        }
469    }
470}
471
472/// Hash the style fields into the provided entries list for map hashing.
473/// Field names match Minecraft's `Style.Serializer.MAP_CODEC`.
474fn hash_format_fields(format: &Format, entries: &mut Vec<HashEntry>) {
475    // color
476    if let Some(color) = &format.color {
477        let mut key_hasher = ComponentHasher::new();
478        key_hasher.put_string("color");
479        let mut value_hasher = ComponentHasher::new();
480        value_hasher.put_string(&color.to_string());
481        entries.push(HashEntry::new(key_hasher, value_hasher));
482    }
483
484    // shadow_color
485    if let Some(shadow_color) = &format.shadow_color {
486        let mut key_hasher = ComponentHasher::new();
487        key_hasher.put_string("shadow_color");
488        let mut value_hasher = ComponentHasher::new();
489        value_hasher.put_int(*shadow_color);
490        entries.push(HashEntry::new(key_hasher, value_hasher));
491    }
492
493    // bold
494    if let Some(bold) = format.bold {
495        let mut key_hasher = ComponentHasher::new();
496        key_hasher.put_string("bold");
497        let mut value_hasher = ComponentHasher::new();
498        value_hasher.put_bool(bold);
499        entries.push(HashEntry::new(key_hasher, value_hasher));
500    }
501
502    // italic
503    if let Some(italic) = format.italic {
504        let mut key_hasher = ComponentHasher::new();
505        key_hasher.put_string("italic");
506        let mut value_hasher = ComponentHasher::new();
507        value_hasher.put_bool(italic);
508        entries.push(HashEntry::new(key_hasher, value_hasher));
509    }
510
511    // underlined
512    if let Some(underlined) = format.underlined {
513        let mut key_hasher = ComponentHasher::new();
514        key_hasher.put_string("underlined");
515        let mut value_hasher = ComponentHasher::new();
516        value_hasher.put_bool(underlined);
517        entries.push(HashEntry::new(key_hasher, value_hasher));
518    }
519
520    // strikethrough
521    if let Some(strikethrough) = format.strikethrough {
522        let mut key_hasher = ComponentHasher::new();
523        key_hasher.put_string("strikethrough");
524        let mut value_hasher = ComponentHasher::new();
525        value_hasher.put_bool(strikethrough);
526        entries.push(HashEntry::new(key_hasher, value_hasher));
527    }
528
529    // obfuscated
530    if let Some(obfuscated) = format.obfuscated {
531        let mut key_hasher = ComponentHasher::new();
532        key_hasher.put_string("obfuscated");
533        let mut value_hasher = ComponentHasher::new();
534        value_hasher.put_bool(obfuscated);
535        entries.push(HashEntry::new(key_hasher, value_hasher));
536    }
537
538    // font (encoded as a string identifier)
539    if let Some(font) = &format.font {
540        let mut key_hasher = ComponentHasher::new();
541        key_hasher.put_string("font");
542        let mut value_hasher = ComponentHasher::new();
543        value_hasher.put_string(font);
544        entries.push(HashEntry::new(key_hasher, value_hasher));
545    }
546}
547
548fn hash_hover_fields(event: &HoverEvent, hasher: &mut ComponentHasher) {
549    let mut entries: Vec<HashEntry> = Vec::new();
550
551    match event {
552        HoverEvent::ShowText { value } => {
553            {
554                let mut key_hasher = ComponentHasher::new();
555                key_hasher.put_string("action");
556                let mut value_hasher = ComponentHasher::new();
557                value_hasher.put_string("show_text");
558                entries.push(HashEntry::new(key_hasher, value_hasher));
559            }
560            {
561                let mut key_hasher = ComponentHasher::new();
562                key_hasher.put_string("value");
563                let mut value_hasher = ComponentHasher::new();
564                value.hash_component(&mut value_hasher);
565                entries.push(HashEntry::new(key_hasher, value_hasher));
566            }
567        }
568        HoverEvent::ShowItem {
569            id,
570            count,
571            components,
572        } => {
573            {
574                let mut key_hasher = ComponentHasher::new();
575                key_hasher.put_string("action");
576                let mut value_hasher = ComponentHasher::new();
577                value_hasher.put_string("show_item");
578                entries.push(HashEntry::new(key_hasher, value_hasher));
579            }
580            {
581                let mut key_hasher = ComponentHasher::new();
582                key_hasher.put_string("id");
583                let mut value_hasher = ComponentHasher::new();
584                value_hasher.put_string(id);
585                entries.push(HashEntry::new(key_hasher, value_hasher));
586            }
587            if *count != 1 {
588                let mut key_hasher = ComponentHasher::new();
589                key_hasher.put_string("count");
590                let mut value_hasher = ComponentHasher::new();
591                value_hasher.put_int(*count);
592                entries.push(HashEntry::new(key_hasher, value_hasher));
593            }
594            if let Some(components) = components
595                && !components.is_empty_compound()
596            {
597                let mut key_hasher = ComponentHasher::new();
598                key_hasher.put_string("components");
599                let mut value_hasher = ComponentHasher::new();
600                // Embedded payloads are already DataComponentPatch::CODEC output.
601                components.as_nbt().hash_component(&mut value_hasher);
602                entries.push(HashEntry::new(key_hasher, value_hasher));
603            }
604        }
605        HoverEvent::ShowEntity { name, id, uuid } => {
606            {
607                let mut key_hasher = ComponentHasher::new();
608                key_hasher.put_string("action");
609                let mut value_hasher = ComponentHasher::new();
610                value_hasher.put_string("show_entity");
611                entries.push(HashEntry::new(key_hasher, value_hasher));
612            }
613            {
614                let mut key_hasher = ComponentHasher::new();
615                key_hasher.put_string("id");
616                let mut value_hasher = ComponentHasher::new();
617                value_hasher.put_string(id);
618                entries.push(HashEntry::new(key_hasher, value_hasher));
619            }
620            {
621                let mut key_hasher = ComponentHasher::new();
622                key_hasher.put_string("uuid");
623                let mut value_hasher = ComponentHasher::new();
624                let (most, least) = uuid.as_u64_pair();
625                value_hasher.put_int_array(&[
626                    (most >> 32) as i32,
627                    most as i32,
628                    (least >> 32) as i32,
629                    least as i32,
630                ]);
631                entries.push(HashEntry::new(key_hasher, value_hasher));
632            }
633            if let Some(name) = name {
634                let mut key_hasher = ComponentHasher::new();
635                key_hasher.put_string("name");
636                let mut value_hasher = ComponentHasher::new();
637                name.hash_component(&mut value_hasher);
638                entries.push(HashEntry::new(key_hasher, value_hasher));
639            }
640        }
641    }
642
643    // Sort entries by key hash, then value hash (Minecraft's map ordering)
644    sort_map_entries(&mut entries);
645
646    // Write the sorted map
647    hasher.start_map();
648    for entry in entries {
649        hasher.put_raw_bytes(&entry.key_bytes);
650        hasher.put_raw_bytes(&entry.value_bytes);
651    }
652    hasher.end_map();
653}
654
655#[expect(
656    clippy::too_many_lines,
657    reason = "each ClickEvent variant requires distinct hashing logic; splitting would hurt readability"
658)]
659fn hash_click_fields(event: &ClickEvent, hasher: &mut ComponentHasher) {
660    let mut entries: Vec<HashEntry> = Vec::new();
661
662    match event {
663        ClickEvent::OpenUrl { url } => {
664            {
665                let mut key_hasher = ComponentHasher::new();
666                key_hasher.put_string("action");
667                let mut value_hasher = ComponentHasher::new();
668                value_hasher.put_string("open_url");
669                entries.push(HashEntry::new(key_hasher, value_hasher));
670            }
671            {
672                let mut key_hasher = ComponentHasher::new();
673                key_hasher.put_string("url");
674                let mut value_hasher = ComponentHasher::new();
675                value_hasher.put_string(url);
676                entries.push(HashEntry::new(key_hasher, value_hasher));
677            }
678        }
679        ClickEvent::RunCommand { command } => {
680            {
681                let mut key_hasher = ComponentHasher::new();
682                key_hasher.put_string("action");
683                let mut value_hasher = ComponentHasher::new();
684                value_hasher.put_string("run_command");
685                entries.push(HashEntry::new(key_hasher, value_hasher));
686            }
687            {
688                let mut key_hasher = ComponentHasher::new();
689                key_hasher.put_string("command");
690                let mut value_hasher = ComponentHasher::new();
691                value_hasher.put_string(command);
692                entries.push(HashEntry::new(key_hasher, value_hasher));
693            }
694        }
695        ClickEvent::SuggestCommand { command } => {
696            {
697                let mut key_hasher = ComponentHasher::new();
698                key_hasher.put_string("action");
699                let mut value_hasher = ComponentHasher::new();
700                value_hasher.put_string("suggest_command");
701                entries.push(HashEntry::new(key_hasher, value_hasher));
702            }
703            {
704                let mut key_hasher = ComponentHasher::new();
705                key_hasher.put_string("command");
706                let mut value_hasher = ComponentHasher::new();
707                value_hasher.put_string(command);
708                entries.push(HashEntry::new(key_hasher, value_hasher));
709            }
710        }
711        ClickEvent::ChangePage { page } => {
712            {
713                let mut key_hasher = ComponentHasher::new();
714                key_hasher.put_string("action");
715                let mut value_hasher = ComponentHasher::new();
716                value_hasher.put_string("change_page");
717                entries.push(HashEntry::new(key_hasher, value_hasher));
718            }
719            {
720                let mut key_hasher = ComponentHasher::new();
721                key_hasher.put_string("page");
722                let mut value_hasher = ComponentHasher::new();
723                value_hasher.put_int(*page);
724                entries.push(HashEntry::new(key_hasher, value_hasher));
725            }
726        }
727        ClickEvent::CopyToClipboard { value } => {
728            {
729                let mut key_hasher = ComponentHasher::new();
730                key_hasher.put_string("action");
731                let mut value_hasher = ComponentHasher::new();
732                value_hasher.put_string("copy_to_clipboard");
733                entries.push(HashEntry::new(key_hasher, value_hasher));
734            }
735            {
736                let mut key_hasher = ComponentHasher::new();
737                key_hasher.put_string("value");
738                let mut value_hasher = ComponentHasher::new();
739                value_hasher.put_string(value);
740                entries.push(HashEntry::new(key_hasher, value_hasher));
741            }
742        }
743        ClickEvent::ShowDialog { dialog } => {
744            {
745                let mut key_hasher = ComponentHasher::new();
746                key_hasher.put_string("action");
747                let mut value_hasher = ComponentHasher::new();
748                value_hasher.put_string("show_dialog");
749                entries.push(HashEntry::new(key_hasher, value_hasher));
750            }
751            {
752                let mut key_hasher = ComponentHasher::new();
753                key_hasher.put_string("dialog");
754                let mut value_hasher = ComponentHasher::new();
755                match dialog {
756                    Dialog::Reference(reference) => value_hasher.put_string(reference),
757                    Dialog::Inline(value) => {
758                        // Embedded payloads are already Dialog::CODEC output.
759                        value.as_nbt().hash_component(&mut value_hasher);
760                    }
761                }
762                entries.push(HashEntry::new(key_hasher, value_hasher));
763            }
764        }
765        ClickEvent::Custom(custom_data) => {
766            {
767                let mut key_hasher = ComponentHasher::new();
768                key_hasher.put_string("action");
769                let mut value_hasher = ComponentHasher::new();
770                value_hasher.put_string("custom");
771                entries.push(HashEntry::new(key_hasher, value_hasher));
772            }
773            if let Payload::Nbt(payload) = &custom_data.payload {
774                let mut key_hasher = ComponentHasher::new();
775                key_hasher.put_string("payload");
776                let mut value_hasher = ComponentHasher::new();
777                payload.as_nbt().hash_component(&mut value_hasher);
778                entries.push(HashEntry::new(key_hasher, value_hasher));
779            }
780            {
781                let mut key_hasher = ComponentHasher::new();
782                key_hasher.put_string("id");
783                let mut value_hasher = ComponentHasher::new();
784                value_hasher.put_string(&custom_data.id);
785                entries.push(HashEntry::new(key_hasher, value_hasher));
786            }
787        }
788    }
789
790    // Sort entries by key hash, then value hash (Minecraft's map ordering)
791    sort_map_entries(&mut entries);
792
793    // Write the sorted map
794    hasher.start_map();
795    for entry in entries {
796        hasher.put_raw_bytes(&entry.key_bytes);
797        hasher.put_raw_bytes(&entry.value_bytes);
798    }
799    hasher.end_map();
800}
801
802#[cfg(test)]
803mod tests {
804    use std::{borrow::Cow, io::Cursor};
805
806    use simdnbt::owned::{NbtList, NbtTag, read_tag};
807    use text_components::{
808        Modifier as _, TextComponent, interactivity::HoverEvent, translation::TranslatedMessage,
809    };
810
811    use crate::serial::{ReadFrom as _, WriteTo as _};
812
813    #[test]
814    fn component_stream_codec_round_trips_an_unnamed_nbt_tag() {
815        let component = TextComponent::plain("hello");
816        let mut encoded = Vec::new();
817        component
818            .write(&mut encoded)
819            .expect("text component should encode");
820
821        assert_eq!(
822            TextComponent::read(&mut Cursor::new(encoded.as_slice()))
823                .expect("text component should decode"),
824            component
825        );
826    }
827
828    #[test]
829    fn component_codec_recursively_collapses_plain_components() {
830        let component = TextComponent::translated(TranslatedMessage {
831            key: Cow::Borrowed("test.message"),
832            fallback: None,
833            args: Some(Box::new([TextComponent::plain("argument")])),
834        })
835        .add_child(TextComponent::plain("child"))
836        .hover_event(HoverEvent::show_text("hover"));
837
838        let NbtTag::Compound(encoded) = component.to_codec_nbt() else {
839            panic!("styled component should encode as a compound");
840        };
841        assert_eq!(
842            encoded.get("with"),
843            Some(&NbtTag::List(NbtList::String(vec!["argument".into()])))
844        );
845        assert_eq!(
846            encoded.get("extra"),
847            Some(&NbtTag::List(NbtList::String(vec!["child".into()])))
848        );
849        let hover = encoded
850            .get("hover_event")
851            .and_then(NbtTag::compound)
852            .expect("hover event should encode as a compound");
853        assert_eq!(hover.get("value"), Some(&NbtTag::String("hover".into())));
854    }
855
856    #[test]
857    fn component_stream_codec_writes_collapsed_string_tag() {
858        let mut encoded = Vec::new();
859        TextComponent::plain("hello")
860            .write(&mut encoded)
861            .expect("text component should encode");
862        let tag = read_tag(&mut Cursor::new(encoded.as_slice()))
863            .expect("encoded component should be NBT");
864
865        assert_eq!(tag, NbtTag::String("hello".into()));
866    }
867}