Skip to main content

steel_core/behavior/items/
compass.rs

1use 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/// Compass item behavior.
24#[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            // TODO: validate unloaded targets once POI persistence exists.
105            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}