Skip to main content

steel_core/behavior/items/
dynamic_name.rs

1use std::borrow::Cow;
2
3use steel_registry::{
4    data_components::vanilla_components::{ITEM_NAME, POTION_CONTENTS},
5    item_stack::ItemStack,
6};
7use text_components::{TextComponent, content::Content, translation::TranslatedMessage};
8
9pub(super) fn default_name(stack: &ItemStack) -> Cow<'_, TextComponent> {
10    stack
11        .get(ITEM_NAME)
12        .map_or_else(|| Cow::Owned(TextComponent::new()), Cow::Borrowed)
13}
14
15pub(super) fn description_id(stack: &ItemStack) -> Option<&str> {
16    let Content::Translate(message) = &stack.item().components.get_ref(ITEM_NAME)?.content else {
17        return None;
18    };
19    Some(message.key.as_ref())
20}
21
22pub(super) fn translated(
23    key: String,
24    args: Option<Box<[TextComponent]>>,
25) -> Cow<'static, TextComponent> {
26    Cow::Owned(
27        TranslatedMessage {
28            key: Cow::Owned(key),
29            fallback: None,
30            args,
31        }
32        .component(),
33    )
34}
35
36pub(super) fn potion_name(stack: &ItemStack) -> Cow<'_, TextComponent> {
37    let Some(contents) = stack.get(POTION_CONTENTS) else {
38        return default_name(stack);
39    };
40    let Some(description_id) = description_id(stack) else {
41        return default_name(stack);
42    };
43    let suffix = contents
44        .custom_name()
45        .or_else(|| contents.potion().map(|potion| potion.value().name))
46        .unwrap_or("empty");
47    translated(format!("{description_id}.effect.{suffix}"), None)
48}
49
50#[cfg(test)]
51mod tests {
52    use steel_registry::{
53        REGISTRY, RegistryExt, RegistryReference,
54        data_components::{
55            components::{GlobalPos, LodestoneTracker, PotionContents},
56            vanilla_components::{
57                BASE_COLOR, ITEM_NAME, LODESTONE_TRACKER, POTION_CONTENTS, PROFILE,
58            },
59        },
60        dye_color::DyeColor,
61        init_vanilla_registry,
62        item_stack::ItemStack,
63        resolvable_profile::{PlayerSkinPatch, ResolvableProfile},
64        vanilla_items,
65    };
66    use steel_utils::{BlockPos, Identifier};
67    use text_components::{TextComponent, content::Content};
68
69    use crate::behavior::{ITEM_BEHAVIORS, init_behaviors};
70
71    fn translated_key(stack: &ItemStack) -> String {
72        let name = ITEM_BEHAVIORS.hover_name(stack);
73        let Content::Translate(message) = &name.content else {
74            panic!("dynamic item name should be translated");
75        };
76        message.key.to_string()
77    }
78
79    #[test]
80    fn every_mc26_dynamic_item_name_override_uses_stack_components() {
81        init_vanilla_registry();
82        init_behaviors();
83        for potion_key in ["long_swiftness", "strong_swiftness"] {
84            let potion = REGISTRY
85                .potions
86                .by_key(&Identifier::vanilla_static(potion_key))
87                .expect("swiftness potion variant should be registered");
88            for (item, expected) in [
89                (
90                    &*vanilla_items::POTION,
91                    "item.minecraft.potion.effect.swiftness",
92                ),
93                (
94                    &*vanilla_items::SPLASH_POTION,
95                    "item.minecraft.splash_potion.effect.swiftness",
96                ),
97                (
98                    &*vanilla_items::LINGERING_POTION,
99                    "item.minecraft.lingering_potion.effect.swiftness",
100                ),
101                (
102                    &*vanilla_items::TIPPED_ARROW,
103                    "item.minecraft.tipped_arrow.effect.swiftness",
104                ),
105            ] {
106                let mut stack = ItemStack::new(item);
107                stack.set(
108                    POTION_CONTENTS,
109                    PotionContents::new(
110                        Some(RegistryReference::new(potion)),
111                        None,
112                        Vec::new(),
113                        None,
114                    ),
115                );
116                assert_eq!(translated_key(&stack), expected);
117            }
118        }
119
120        let mut compass = ItemStack::new(&vanilla_items::COMPASS);
121        compass.set(
122            LODESTONE_TRACKER,
123            LodestoneTracker::new(
124                Some(GlobalPos::new(
125                    Identifier::vanilla_static("overworld"),
126                    BlockPos::new(1, 2, 3),
127                )),
128                true,
129            ),
130        );
131        assert_eq!(translated_key(&compass), "item.minecraft.lodestone_compass");
132
133        let mut shield = ItemStack::new(&vanilla_items::SHIELD);
134        shield.set(BASE_COLOR, DyeColor::Blue);
135        assert_eq!(translated_key(&shield), "item.minecraft.shield.blue");
136
137        let mut head = ItemStack::new(&vanilla_items::PLAYER_HEAD);
138        head.set(
139            PROFILE,
140            ResolvableProfile::dynamic_name("Notch".to_owned(), PlayerSkinPatch::default())
141                .expect("valid profile name"),
142        );
143        assert_eq!(translated_key(&head), "block.minecraft.player_head.named");
144        let name = ITEM_BEHAVIORS.hover_name(&head);
145        let Content::Translate(message) = &name.content else {
146            panic!("named player head should be translated");
147        };
148        assert_eq!(
149            message
150                .args
151                .as_deref()
152                .and_then(|args| args.first())
153                .map(ToString::to_string)
154                .as_deref(),
155            Some("Notch")
156        );
157    }
158
159    #[test]
160    fn air_name_ignores_stack_item_name_patch() {
161        init_vanilla_registry();
162        init_behaviors();
163        let mut air = ItemStack::new(&vanilla_items::AIR);
164        air.set(ITEM_NAME, TextComponent::plain("patched"));
165        let Some(prototype_name) = vanilla_items::AIR.components.get_ref(ITEM_NAME) else {
166            panic!("air item prototype should have a name");
167        };
168
169        assert_eq!(ITEM_BEHAVIORS.hover_name(&air).as_ref(), prototype_name);
170    }
171
172    #[test]
173    fn dynamic_name_prefix_ignores_stack_item_name_patch() {
174        init_vanilla_registry();
175        init_behaviors();
176        let healing = REGISTRY
177            .potions
178            .by_key(&Identifier::vanilla_static("healing"))
179            .expect("healing potion should be registered");
180        let mut potion = ItemStack::new(&vanilla_items::POTION);
181        potion.set(
182            POTION_CONTENTS,
183            PotionContents::new(
184                Some(RegistryReference::new(healing)),
185                None,
186                Vec::new(),
187                None,
188            ),
189        );
190        potion.set(ITEM_NAME, TextComponent::plain("patched"));
191
192        assert_eq!(
193            translated_key(&potion),
194            "item.minecraft.potion.effect.healing"
195        );
196    }
197}