steel_core/behavior/items/
dynamic_name.rs1use 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}