Skip to main content

steel_core/behavior/items/
spawn_egg.rs

1//! Data-driven vanilla spawn-egg behavior.
2
3use std::sync::Arc;
4
5use steel_macros::item_behavior;
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_registry::data_components::components::EntityData;
8use steel_registry::data_components::vanilla_components::ENTITY_DATA;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::item_stack::ItemStack;
11use steel_registry::stat::vanilla_stat_types;
12use steel_registry::{vanilla_game_events, vanilla_game_rules};
13use steel_utils::types::Difficulty;
14use steel_utils::{BlockPos, translations};
15use text_components::TextComponent;
16
17use crate::behavior::item_utils::get_player_pov_hit_result;
18use crate::behavior::{
19    BLOCK_BEHAVIORS, BlockCollisionContext, BlockStateBehaviorExt as _, ITEM_BEHAVIORS,
20    InteractionResult, InventoryAccess, ItemBehavior, UseItemContext, UseOnContext,
21};
22use crate::entity::{
23    EntitySpawnPlacement, EntitySpawnReason, EntitySpawnRequest, Mob, SharedEntity,
24    add_spawned_entity, apply_implicit_item_stack_components, create_entity_instance, spawn_entity,
25};
26use crate::player::Player;
27use crate::world::ClipFluid;
28use crate::world::World;
29use crate::world::game_event::GameEventContext;
30
31/// Behavior for every data-driven `SpawnEggItem` registry entry.
32#[item_behavior(class = "SpawnEggItem")]
33pub struct SpawnEggItem;
34
35impl SpawnEggItem {
36    fn entity_type(stack: &ItemStack) -> Option<EntityTypeRef> {
37        stack.get(ENTITY_DATA).map(EntityData::entity_type)
38    }
39
40    fn spawn_mob(
41        world: &Arc<World>,
42        player: &Player,
43        inventory: &InventoryAccess,
44        stack: &ItemStack,
45        spawn_pos: BlockPos,
46        try_move_down: bool,
47        moved_up: bool,
48    ) -> InteractionResult {
49        let Some(entity_type) = Self::entity_type(stack) else {
50            return InteractionResult::Fail;
51        };
52
53        let request = EntitySpawnRequest {
54            entity_type,
55            placement: EntitySpawnPlacement::Block {
56                pos: spawn_pos,
57                try_move_down,
58                moved_up,
59            },
60            reason: EntitySpawnReason::SpawnItemUse,
61            finalize_spawn: true,
62            play_ambient_sound: true,
63            item_stack: Some(stack),
64            user_is_operator: player.is_operator(),
65        };
66        if spawn_entity(world, request).is_err() {
67            return InteractionResult::Fail;
68        }
69
70        inventory.with_item(|item| item.consume_one(player.has_infinite_materials()));
71        world.game_event(
72            &vanilla_game_events::ENTITY_PLACE,
73            spawn_pos,
74            &GameEventContext::new(Some(player), None),
75        );
76        InteractionResult::Success
77    }
78
79    /// Runs the interaction before normal mob handling.
80    pub(crate) fn interact_with_mob<M: Mob + ?Sized>(
81        stack: &mut ItemStack,
82        player: &Player,
83        parent: &M,
84    ) -> InteractionResult {
85        if ITEM_BEHAVIORS
86            .get_behavior(stack.item())
87            .as_spawn_egg()
88            .is_none()
89        {
90            return InteractionResult::Pass;
91        }
92        if Self::spawn_offspring(stack, parent).is_none() {
93            return InteractionResult::Pass;
94        }
95        stack.consume_one(player.has_infinite_materials());
96        InteractionResult::SuccessServer
97    }
98
99    fn spawn_offspring<M: Mob + ?Sized>(stack: &ItemStack, parent: &M) -> Option<SharedEntity> {
100        let entity_type = Self::entity_type(stack)?;
101        if entity_type != parent.entity_type() {
102            return None;
103        }
104
105        let world = parent.level()?;
106        let offspring = if let Some(ageable) = parent.as_ageable_mob() {
107            ageable.get_breed_offspring(&world, ageable)?
108        } else {
109            match create_entity_instance(&world, entity_type, parent.position()) {
110                Ok(offspring) => offspring,
111                Err(error) => {
112                    log::warn!(
113                        "Failed to create spawn-egg offspring {} at {:?}: {error:?}",
114                        entity_type.key,
115                        parent.position()
116                    );
117                    return None;
118                }
119            }
120        };
121
122        let mob = offspring.as_mob()?;
123        mob.set_baby(true);
124        if !mob.is_baby() {
125            return None;
126        }
127
128        offspring.base().set_position_local(parent.position());
129        offspring.set_rotation((0.0, 0.0));
130        offspring.set_old_position_to_current();
131        offspring.base().set_old_rotation_to_current();
132        apply_implicit_item_stack_components(&offspring, stack);
133        add_spawned_entity(&world, Arc::clone(&offspring)).ok()?;
134        Some(offspring)
135    }
136}
137
138impl ItemBehavior for SpawnEggItem {
139    fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
140        Some(self)
141    }
142
143    fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
144        let stack = context.inv.with_item(|item| item.clone());
145        let Some(entity_type) = Self::entity_type(&stack) else {
146            return InteractionResult::Fail;
147        };
148        if context.world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful {
149            return InteractionResult::Fail;
150        }
151
152        let clicked_pos = context.hit_result.block_pos;
153        let clicked_state = context.world.get_block_state(clicked_pos);
154        if let Some(block_entity) = context.world.get_block_entity(clicked_pos)
155            && let Some(spawner) = block_entity.as_spawner()
156        {
157            if !context
158                .world
159                .get_game_rule(&vanilla_game_rules::SPAWNER_BLOCKS_WORK)
160            {
161                context.player.send_message(&TextComponent::translated(
162                    translations::ADV_MODE_NOT_ENABLED_SPAWNER.msg(),
163                ));
164                return InteractionResult::Fail;
165            }
166
167            if context.player.is_operator()
168                && let Some(entity_data) = stack.get(ENTITY_DATA)
169            {
170                spawner.set_entity_data(entity_data);
171            } else {
172                spawner.set_entity_id(entity_type);
173            }
174            context.inv.with_item(|item| item.shrink(1));
175            context.world.game_event(
176                &vanilla_game_events::BLOCK_CHANGE,
177                clicked_pos,
178                &GameEventContext::new(Some(context.player.as_ref()), None),
179            );
180            return InteractionResult::Success;
181        }
182
183        let clicked_face = context.hit_result.direction;
184        let shape = BLOCK_BEHAVIORS
185            .get_behavior(clicked_state.get_block())
186            .get_collision_shape(
187                clicked_state,
188                context.world.as_ref(),
189                clicked_pos,
190                BlockCollisionContext::empty(),
191            );
192        let spawn_pos = if shape.is_empty() {
193            clicked_pos
194        } else {
195            clicked_face.relative(clicked_pos)
196        };
197
198        Self::spawn_mob(
199            context.world,
200            context.player,
201            &context.inv,
202            &stack,
203            spawn_pos,
204            true,
205            spawn_pos != clicked_pos && clicked_face == steel_utils::Direction::Up,
206        )
207    }
208
209    fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
210        let hit_result =
211            get_player_pov_hit_result(context.world, context.player, ClipFluid::SourceOnly);
212        if hit_result.miss {
213            return InteractionResult::Pass;
214        }
215
216        let stack = context.inv.with_item(|item| item.clone());
217        if Self::entity_type(&stack).is_none() {
218            return InteractionResult::Fail;
219        }
220
221        let pos = hit_result.block_pos;
222        let state = context.world.get_block_state(pos);
223        if !state.is_liquid_block() {
224            return InteractionResult::Pass;
225        }
226        if !context.world.may_interact(context.player, pos)
227            || !context
228                .player
229                .may_use_item_at(pos, hit_result.direction, &stack)
230        {
231            return InteractionResult::Fail;
232        }
233
234        let result = Self::spawn_mob(
235            context.world,
236            context.player,
237            &context.inv,
238            &stack,
239            pos,
240            false,
241            false,
242        );
243        if result == InteractionResult::Success {
244            context
245                .player
246                .award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
247        }
248        result
249    }
250}