Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
65646c8
spawn egg
chocodev11 Aug 21, 2026
347507b
consume and formating, im stupid yk?
chocodev11 Aug 22, 2026
6fa3cfc
remove hardcode
chocodev11 Aug 22, 2026
91b16d8
todo
chocodev11 Aug 22, 2026
8da3a4c
Merge branch 'master' into feat/spawn-egg
chocodev11 Aug 22, 2026
6e320b4
Merge remote-tracking branch 'upstream' into feat/spawn-egg
chocodev11 Aug 23, 2026
8668091
Merge branch 'feat/spawn-egg' of https://github.com/chocodev11/SteelM…
chocodev11 Aug 23, 2026
7f1bdf2
fix lint
chocodev11 Aug 23, 2026
50a0d20
formating
chocodev11 Aug 23, 2026
c3fe9e9
stat
chocodev11 Aug 23, 2026
bb7f148
format
chocodev11 Aug 23, 2026
24bb313
function for coco
chocodev11 Aug 26, 2026
531bd79
a greatful warn
chocodev11 Aug 26, 2026
22d6a86
sync consume
chocodev11 Aug 26, 2026
c1b4fb7
name tag order
chocodev11 Aug 26, 2026
813b161
Merge remote-tracking branch 'upstream' into feat/spawn-egg
chocodev11 Aug 26, 2026
cdcfc17
custom name spawn egg
chocodev11 Aug 26, 2026
82deb90
Merge branch 'master' into feat/spawn-egg
chocodev11 Aug 27, 2026
745a584
formating
chocodev11 Aug 27, 2026
73209bf
magic cucumber
chocodev11 Aug 27, 2026
ec352c6
stat and item check
chocodev11 Aug 27, 2026
0523f94
off spring spawn
chocodev11 Aug 27, 2026
29a55d0
position helper
chocodev11 Aug 27, 2026
c04aaff
item stack component and may_use_item_at
chocodev11 Aug 28, 2026
e43db9d
fix non-op exploit
chocodev11 Aug 28, 2026
90ae67a
Merge branch 'master' into feat/spawn-egg
coco875 Aug 28, 2026
381958d
Merge remote-tracking branch 'upstream' into feat/spawn-egg
chocodev11 Aug 28, 2026
e04e1f5
Merge branch 'feat/spawn-egg' of https://github.com/chocodev11/SteelM…
chocodev11 Aug 28, 2026
45edfaa
add more mob component
chocodev11 Aug 31, 2026
83ad3b1
Merge remote-tracking branch 'upstream' into feat/spawn-egg
chocodev11 Sep 3, 2026
7d6e1f7
im stupid
chocodev11 Sep 4, 2026
8577d03
Merge branch 'master' into feat/spawn-egg
JunkyDeveloper Sep 7, 2026
f8de499
move work to spawner PR
JunkyDeveloper Sep 7, 2026
89c801c
Merge remote-tracking branch 'chocodev11/feat/spawn-egg' into pr/412
JunkyDeveloper Sep 7, 2026
ac10bf7
fix merge conflicts + use consume instead of shrink
JunkyDeveloper Sep 7, 2026
6b3e5df
fmt
JunkyDeveloper Sep 7, 2026
698b34b
will not compile only to pitch idea
JunkyDeveloper Sep 8, 2026
00a6812
fix only_op_can_set_nbt
JunkyDeveloper Sep 8, 2026
a93b3d8
fixed
JunkyDeveloper Sep 8, 2026
b618883
fmt
JunkyDeveloper Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions steel-core/src/behavior/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,11 @@ pub trait BlockBehavior: Send + Sync {
false
}

/// Returns whether this behavior implements vanilla `LiquidBlock`.
fn is_liquid_block(&self) -> bool {
false
}

/// Mirrors vanilla `DoorBlock.isWoodenDoor`.
///
/// Despite the vanilla name, this returns true for any door block type that
Expand Down
20 changes: 20 additions & 0 deletions steel-core/src/behavior/blocks/fluid/liquid_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ impl LiquidBlock {
}

impl BlockBehavior for LiquidBlock {
fn is_liquid_block(&self) -> bool {
true
}

fn get_state_for_placement(&self, _context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
Some(self.block.default_state())
}
Expand Down Expand Up @@ -357,4 +361,20 @@ mod tests {
assert_eq!(updated, state);
assert!(level.scheduled_block_ticks.borrow().is_empty());
}

#[test]
fn liquid_block_behavior_and_ext_identify_liquid_blocks() {
use crate::behavior::BlockStateBehaviorExt as _;

init_vanilla_registry();
init_behaviors();

let liquid = LiquidBlock::new(&vanilla_blocks::WATER, &vanilla_fluids::WATER);
assert!(liquid.is_liquid_block());

assert!(vanilla_blocks::WATER.default_state().is_liquid_block());
assert!(vanilla_blocks::LAVA.default_state().is_liquid_block());
assert!(!vanilla_blocks::STONE.default_state().is_liquid_block());
assert!(!vanilla_blocks::AIR.default_state().is_liquid_block());
}
}
7 changes: 6 additions & 1 deletion steel-core/src/behavior/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, sound_events, vanilla
use steel_utils::types::InteractionHand;
use text_components::TextComponent;

use crate::behavior::items::DefaultItemBehavior;
use crate::behavior::items::{DefaultItemBehavior, SpawnEggItem};
use crate::behavior::{InteractionResult, UseItemContext, UseOnContext};
use crate::entity::consume_effect::apply_consume_effect;
use crate::entity::damage::DamageSource;
Expand Down Expand Up @@ -224,6 +224,11 @@ pub trait ItemBehavior: Send + Sync {
.get_weapon()
.map(|weapon| weapon.item_damage_per_attack)
}

/// Returns this item behavior as a `SpawnEggItem`.
fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
None
}
}

fn should_emit_consume_particles_and_sounds(consumable: &Consumable, ticks_remaining: i32) -> bool {
Expand Down
2 changes: 2 additions & 0 deletions steel-core/src/behavior/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod shovel;
mod sign_item;
mod snowball;
mod solid_bucket_item;
mod spawn_egg;
mod spyglass;
mod standing_and_wall_block_item;
mod throwable_potion;
Expand Down Expand Up @@ -79,6 +80,7 @@ pub use shovel::ShovelItem;
pub use sign_item::{HangingSignItem, SignItem};
pub use snowball::SnowballItem;
pub use solid_bucket_item::SolidBucketItem;
pub use spawn_egg::SpawnEggItem;
pub use spyglass::SpyglassItem;
pub use standing_and_wall_block_item::StandingAndWallBlockItem;
pub use throwable_potion::{LingeringPotionItem, SplashPotionItem};
Expand Down
229 changes: 229 additions & 0 deletions steel-core/src/behavior/items/spawn_egg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//! Data-driven vanilla spawn-egg behavior.

use std::sync::Arc;

use steel_macros::item_behavior;
use steel_registry::blocks::block_state_ext::BlockStateExt as _;
use steel_registry::data_components::components::EntityData;
use steel_registry::data_components::vanilla_components::ENTITY_DATA;
use steel_registry::entity_type::EntityTypeRef;
use steel_registry::item_stack::ItemStack;
use steel_registry::stat::vanilla_stat_types;
use steel_registry::{vanilla_blocks, vanilla_game_events};
use steel_utils::BlockPos;

use crate::behavior::item_utils::get_player_pov_hit_result;
use crate::behavior::{
BLOCK_BEHAVIORS, BlockCollisionContext, BlockStateBehaviorExt as _, ITEM_BEHAVIORS,
InteractionResult, InventoryAccess, ItemBehavior, UseItemContext, UseOnContext,
};
use crate::entity::{
AgeableMob, EntitySpawnPlacement, EntitySpawnReason, EntitySpawnRequest, Mob, SharedEntity,
add_spawned_entity, apply_implicit_item_stack_components, create_entity_instance, spawn_entity,
};
use crate::player::Player;
use crate::world::ClipFluid;
use crate::world::World;
use crate::world::game_event::GameEventContext;

/// Behavior for every data-driven `SpawnEggItem` registry entry.
#[item_behavior(class = "SpawnEggItem")]
pub struct SpawnEggItem;

impl SpawnEggItem {
fn entity_type(stack: &ItemStack) -> Option<EntityTypeRef> {
stack.get(ENTITY_DATA).map(EntityData::entity_type)
}

fn spawn_mob(
world: &Arc<World>,
player: &Player,
inventory: &InventoryAccess,
stack: &ItemStack,
spawn_pos: BlockPos,
try_move_down: bool,
moved_up: bool,
) -> InteractionResult {
let Some(entity_type) = Self::entity_type(stack) else {
return InteractionResult::Fail;
};

let request = EntitySpawnRequest {
entity_type,
placement: EntitySpawnPlacement::Block {
pos: spawn_pos,
try_move_down,
moved_up,
},
reason: EntitySpawnReason::SpawnItemUse,
finalize_spawn: true,
play_ambient_sound: true,
item_stack: Some(stack),
user_is_operator: player.is_operator(),
};
if spawn_entity(world, request).is_err() {
return InteractionResult::Fail;
}

inventory.with_item(|item| item.consume_one(player.has_infinite_materials()));
world.game_event(
&vanilla_game_events::ENTITY_PLACE,
spawn_pos,
&GameEventContext::new(Some(player), None),
);
InteractionResult::Success
}

/// Runs the interaction before normal mob handling.
pub(crate) fn interact_with_mob<M: Mob + ?Sized>(
stack: &mut ItemStack,
player: &Player,
parent: &M,
) -> InteractionResult {
if ITEM_BEHAVIORS
.get_behavior(stack.item())
.as_spawn_egg()
.is_none()
{
return InteractionResult::Pass;
}
if Self::spawn_offspring(stack, parent).is_none() {
return InteractionResult::Pass;
}
stack.consume_one(player.has_infinite_materials());
InteractionResult::SuccessServer
}

fn spawn_offspring<M: Mob + ?Sized>(stack: &ItemStack, parent: &M) -> Option<SharedEntity> {
let entity_type = Self::entity_type(stack)?;
if entity_type != parent.entity_type() {
return None;
}

let world = parent.level()?;
let offspring = if let Some(animal) = parent.as_animal() {
Comment thread
chocodev11 marked this conversation as resolved.
Resolved
animal.get_breed_offspring(&world, animal)?
} else {
match create_entity_instance(&world, entity_type, parent.position()) {
Ok(offspring) => offspring,
Err(error) => {
log::warn!(
"Failed to create spawn-egg offspring {} at {:?}: {error:?}",
entity_type.key,
parent.position()
);
return None;
}
}
};

let ageable = offspring.as_ageable_mob()?;
ageable.set_baby(true);
if !AgeableMob::is_baby(ageable) {
return None;
}

offspring.base().set_position_local(parent.position());
offspring.set_rotation((0.0, 0.0));
offspring.set_old_position_to_current();
offspring.base().set_old_rotation_to_current();
apply_implicit_item_stack_components(&offspring, stack);
add_spawned_entity(&world, offspring.clone()).ok()?;
Some(offspring)
}
}
Comment thread
chocodev11 marked this conversation as resolved.
Resolved

impl ItemBehavior for SpawnEggItem {
fn as_spawn_egg(&self) -> Option<&SpawnEggItem> {
Some(self)
}

fn use_on(&self, context: &mut UseOnContext) -> InteractionResult {
let stack = context.inv.with_item(|item| item.clone());
if Self::entity_type(&stack).is_none() {
return InteractionResult::Fail;
}

let clicked_pos = context.hit_result.block_pos;
let clicked_state = context.world.get_block_state(clicked_pos);
if clicked_state.get_block() == &vanilla_blocks::SPAWNER {
Comment thread
JunkyDeveloper marked this conversation as resolved.
Resolved
// TODO: Use spawn eggs on a spawner block and also check if the condition is correct
// Spawner block-entity mutation is a separate foundation; do not
// incorrectly create a mob beside a spawner in its place.
return InteractionResult::Fail;
}
Comment thread
coco875 marked this conversation as resolved.
Resolved

let clicked_face = context.hit_result.direction;
let shape = BLOCK_BEHAVIORS
.get_behavior(clicked_state.get_block())
.get_collision_shape(
clicked_state,
context.world.as_ref(),
clicked_pos,
BlockCollisionContext::empty(),
);
let spawn_pos = if shape.is_empty() {
clicked_pos
} else {
clicked_face.relative(clicked_pos)
};

let result = Self::spawn_mob(
context.world,
context.player,
&context.inv,
&stack,
spawn_pos,
true,
spawn_pos != clicked_pos && clicked_face == steel_utils::Direction::Up,
);
if result == InteractionResult::Success {
context
.player
.award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
}
result
}

fn use_item(&self, context: &mut UseItemContext) -> InteractionResult {
let hit_result =
get_player_pov_hit_result(context.world, context.player, ClipFluid::SourceOnly);
if hit_result.miss {
return InteractionResult::Pass;
}

let stack = context.inv.with_item(|item| item.clone());
if Self::entity_type(&stack).is_none() {
return InteractionResult::Fail;
}

let pos = hit_result.block_pos;
let state = context.world.get_block_state(pos);
if !state.is_liquid_block() {
return InteractionResult::Pass;
}
if !context.world.may_interact(context.player, pos)
|| !context
.player
.may_use_item_at(pos, hit_result.direction, &stack)
{
return InteractionResult::Fail;
}

let result = Self::spawn_mob(
context.world,
context.player,
&context.inv,
&stack,
pos,
false,
false,
);
if result == InteractionResult::Success {
context
.player
.award_stat(&vanilla_stat_types::ITEM_USED, stack.item());
}
result
}
}
8 changes: 8 additions & 0 deletions steel-core/src/behavior/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ pub trait BlockStateBehaviorExt {
/// Returns whether this block state extends `BedBlock`
fn is_bed(&self) -> bool;

/// Returns whether this block state extends `LiquidBlock`.
fn is_liquid_block(&self) -> bool;

/// Returns whether this block state can be occupied by a forced respawn position
fn is_possible_to_respawn_in_this(&self) -> bool;
}
Expand Down Expand Up @@ -182,6 +185,11 @@ impl BlockStateBehaviorExt for BlockStateId {
BLOCK_BEHAVIORS.get_behavior(block).is_bed()
}

fn is_liquid_block(&self) -> bool {
let block = self.get_block();
BLOCK_BEHAVIORS.get_behavior(block).is_liquid_block()
}

fn is_possible_to_respawn_in_this(&self) -> bool {
let block = self.get_block();
let behavior = BLOCK_BEHAVIORS.get_behavior(block);
Expand Down
20 changes: 2 additions & 18 deletions steel-core/src/chunk_saver/storage/entities.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::entity::clamp_loaded_entity_position;

impl ChunkStorage {
pub(super) fn entities_to_persistent(entities: &[SharedEntity]) -> Vec<PersistentEntity> {
Expand Down Expand Up @@ -116,23 +117,6 @@ impl ChunkStorage {
}
}

pub(super) fn clamp_loaded_entity_position(pos: DVec3) -> DVec3 {
DVec3::new(
pos.x.clamp(
-ENTITY_LOAD_MAX_HORIZONTAL_POSITION,
ENTITY_LOAD_MAX_HORIZONTAL_POSITION,
),
pos.y.clamp(
-ENTITY_LOAD_MAX_VERTICAL_POSITION,
ENTITY_LOAD_MAX_VERTICAL_POSITION,
),
pos.z.clamp(
-ENTITY_LOAD_MAX_HORIZONTAL_POSITION,
ENTITY_LOAD_MAX_HORIZONTAL_POSITION,
),
)
}

pub(super) fn entity_to_persistent(
entity: &SharedEntity,
visited: &mut FxHashSet<i32>,
Expand Down Expand Up @@ -369,7 +353,7 @@ impl ChunkStorage {
return None;
}

let pos = Self::clamp_loaded_entity_position(stored_pos);
let pos = clamp_loaded_entity_position(stored_pos);

// Validate position is within expected chunk (sanity check)
let expected_chunk = ChunkPos::from_entity_pos(pos);
Expand Down
3 changes: 0 additions & 3 deletions steel-core/src/chunk_saver/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ mod ticks;
#[cfg(test)]
mod tests;

const ENTITY_LOAD_MAX_HORIZONTAL_POSITION: f64 = 3.000_051_2E7;
const ENTITY_LOAD_MAX_VERTICAL_POSITION: f64 = 2.0E7;

/// Converts `Option<Direction>` to the vanilla 2D data value encoding for persistence.
/// -1 = none, 0 = south, 1 = west, 2 = north, 3 = east.
const fn direction_to_2d(dir: Option<Direction>) -> i8 {
Expand Down
3 changes: 3 additions & 0 deletions steel-core/src/chunk_saver/storage/tests/entities.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::entity::{ENTITY_LOAD_MAX_HORIZONTAL_POSITION, ENTITY_LOAD_MAX_VERTICAL_POSITION};
use steel_registry::RegistryEntry as _;

fn test_persistent_end_crystal(pos: DVec3) -> PersistentEntity {
Expand Down Expand Up @@ -50,6 +51,8 @@ fn persistent_entity_load_clamps_position_like_vanilla() {
-ENTITY_LOAD_MAX_HORIZONTAL_POSITION,
)
);
assert_eq!(entity.base().old_position(), entity.position());
assert_eq!(entity.base().old_rotation(), entity.rotation());
}

#[test]
Expand Down
Loading
Loading