Skip to main content

steel_utils/text/
mod.rs

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