steel_core/behavior/items/
compass.rs1use crate::player::player_inventory::Prediction;
2use std::borrow::Cow;
3
4use steel_macros::item_behavior;
5use steel_protocol::packets::game::SoundSource;
6use steel_registry::{
7 RegistryEntry as _,
8 blocks::block_state_ext::BlockStateExt,
9 data_components::{
10 components::{GlobalPos, LodestoneTracker},
11 vanilla_components::LODESTONE_TRACKER,
12 },
13 item_stack::ItemStack,
14 sound_events, vanilla_blocks, vanilla_poi_types,
15};
16use text_components::TextComponent;
17
18use crate::behavior::{InteractionResult, InventoryTickContext, ItemBehavior, UseOnContext};
19use crate::inventory::container::Container;
20
21use super::dynamic_name::{default_name, translated};
22
23#[item_behavior]
25pub struct CompassItem;
26
27impl ItemBehavior for CompassItem {
28 fn get_name<'a>(&self, stack: &'a ItemStack) -> Cow<'a, TextComponent> {
29 if stack.has(LODESTONE_TRACKER) {
30 translated("item.minecraft.lodestone_compass".to_owned(), None)
31 } else {
32 default_name(stack)
33 }
34 }
35
36 fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
37 let pos = context.hit_result.block_pos;
38 let block_state = context.world.get_block_state(pos);
39
40 if block_state.get_block() != &vanilla_blocks::LODESTONE {
41 return InteractionResult::Pass;
42 }
43
44 context.world.play_sound(
45 &sound_events::ITEM_LODESTONE_COMPASS_LOCK,
46 SoundSource::Players,
47 pos,
48 1.0,
49 1.0,
50 None,
51 );
52
53 let dimension = context.world.key.clone();
54 let tracker = LodestoneTracker::new(Some(GlobalPos::new(dimension, pos)), true);
55
56 let has_infinite_materials = context.player.has_infinite_materials();
57
58 let leftover = context.inv.with_inventory(|inv| {
59 if !has_infinite_materials && inv.get_item_in_hand(context.hand).count() == 1 {
60 inv.mutate_item_in_hand(context.hand, |item| item.set(LODESTONE_TRACKER, tracker));
61 return ItemStack::empty();
62 }
63
64 let mut lodestone_compass = inv.mutate_item_in_hand(context.hand, |item| {
65 item.consume_and_return(1, has_infinite_materials)
66 });
67 lodestone_compass.set(LODESTONE_TRACKER, tracker);
68 if inv.add(&mut lodestone_compass) || has_infinite_materials {
69 ItemStack::empty()
70 } else {
71 lodestone_compass
72 }
73 });
74
75 if !leftover.is_empty() {
76 let _ = context
77 .player
78 .drop_item(leftover, false, Prediction::Predicted);
79 }
80
81 InteractionResult::Success
82 }
83
84 fn inventory_tick(&self, context: &mut InventoryTickContext<'_>) {
85 let Some(target) = context
86 .with_item(|stack| {
87 stack
88 .get(LODESTONE_TRACKER)
89 .filter(|tracker| tracker.tracked())
90 .and_then(|tracker| tracker.target().cloned())
91 })
92 .flatten()
93 else {
94 return;
95 };
96
97 let world = context.world;
98 if world.key != *target.dimension() {
99 return;
100 }
101
102 let pos = target.pos();
103 if world.is_in_world_bounds(pos) {
104 if !world.is_full_chunk_loaded_at(pos) {
106 return;
107 }
108 let lodestone_exists = vanilla_poi_types::LODESTONE
109 .try_id()
110 .is_some_and(|id| world.poi_storage.lock().get_type(pos) == Some(id));
111 if lodestone_exists {
112 return;
113 }
114 }
115
116 context.with_item(|stack| stack.set(LODESTONE_TRACKER, LodestoneTracker::new(None, true)));
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::sync::Arc;
123
124 use super::*;
125 use crate::behavior::init_behaviors;
126 use crate::entity::{Entity, LivingEntity};
127 use crate::test_support::{TestPlayerBuilder, fresh_test_world, insert_ready_full_chunk};
128 use glam::DVec3;
129 use steel_registry::blocks::properties::Direction;
130 use steel_registry::items::item::BlockHitResult;
131 use steel_registry::{init_vanilla_registry, vanilla_items};
132 use steel_utils::types::InteractionHand;
133 use steel_utils::types::UpdateFlags;
134 use steel_utils::{BlockPos, ChunkPos};
135 use uuid::Uuid;
136
137 #[test]
138 fn compass_binds_to_lodestone_and_invalidates_when_removed() {
139 init_vanilla_registry();
140 init_behaviors();
141
142 let world_fixture = fresh_test_world("compass_test_world");
143 let world = &world_fixture.world;
144 let pos = BlockPos::new(0, 64, 0);
145 let chunk_pos = ChunkPos::from_block_pos(pos);
146 insert_ready_full_chunk(world, chunk_pos);
147
148 assert!(world.set_block(
149 pos,
150 vanilla_blocks::LODESTONE.default_state(),
151 UpdateFlags::UPDATE_ALL
152 ));
153
154 let player = TestPlayerBuilder::new(Arc::clone(world), "CompassTester", 1)
155 .uuid(Uuid::from_u128(1))
156 .build();
157
158 player
159 .inventory
160 .lock()
161 .set_selected_item(ItemStack::new(&vanilla_items::COMPASS));
162
163 let hit_result = BlockHitResult {
164 location: DVec3::new(0.5, 64.5, 0.5),
165 direction: Direction::Up,
166 block_pos: pos,
167 miss: false,
168 inside: false,
169 world_border_hit: false,
170 };
171 let mut context = UseOnContext::new(
172 &player,
173 InteractionHand::MainHand,
174 hit_result,
175 world,
176 Arc::clone(&player.inventory),
177 );
178
179 let behavior = CompassItem;
180
181 let result = behavior.use_on(&mut context);
182 assert_eq!(result, InteractionResult::Success);
183
184 let compass = player.inventory.lock().get_selected_item().clone();
185 let tracker = compass
186 .get(LODESTONE_TRACKER)
187 .expect("should have lodestone tracker component");
188 assert!(tracker.tracked());
189 let target = tracker.target().expect("should have target global pos");
190 assert_eq!(target.pos(), pos);
191 assert_eq!(*target.dimension(), world.key);
192
193 Arc::clone(&player).tick();
194 let compass = player.inventory.lock().get_selected_item().clone();
195 let tracker = compass
196 .get(LODESTONE_TRACKER)
197 .expect("should keep lodestone tracker component");
198 assert!(tracker.target().is_some());
199
200 assert!(world.set_block(
201 pos,
202 vanilla_blocks::AIR.default_state(),
203 UpdateFlags::UPDATE_ALL
204 ));
205 Arc::clone(&player).tick();
206 let compass = player.inventory.lock().get_selected_item().clone();
207 let tracker = compass
208 .get(LODESTONE_TRACKER)
209 .expect("should keep lodestone tracker component");
210 assert!(
211 tracker.target().is_none(),
212 "target should be invalidated and set to None"
213 );
214 }
215
216 #[test]
217 fn compass_invalidates_target_outside_world_bounds() {
218 init_vanilla_registry();
219 init_behaviors();
220
221 let world_fixture = fresh_test_world("compass_bounds_test_world");
222 let world = &world_fixture.world;
223 let player = TestPlayerBuilder::new(Arc::clone(world), "CompassTester", 1)
224 .uuid(Uuid::from_u128(1))
225 .build();
226
227 let invalid_pos = BlockPos::new(40_000_000, 64, 0);
228 let tracker =
229 LodestoneTracker::new(Some(GlobalPos::new(world.key.clone(), invalid_pos)), true);
230
231 let mut compass = ItemStack::new(&vanilla_items::COMPASS);
232 compass.set(LODESTONE_TRACKER, tracker);
233
234 player.inventory.lock().set_offhand_item(compass);
235 LivingEntity::tick_equipment(player.as_ref());
236
237 let compass = player.inventory.lock().get_offhand_item().clone();
238 let tracker = compass
239 .get(LODESTONE_TRACKER)
240 .expect("should keep lodestone tracker component");
241 assert!(
242 tracker.target().is_none(),
243 "target should be immediately invalidated and set to None"
244 );
245 }
246}