diff --git a/steel-core/src/behavior/block/mod.rs b/steel-core/src/behavior/block/mod.rs index 6f76efc6c31..97ba1aff84b 100644 --- a/steel-core/src/behavior/block/mod.rs +++ b/steel-core/src/behavior/block/mod.rs @@ -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 diff --git a/steel-core/src/behavior/blocks/fluid/liquid_block.rs b/steel-core/src/behavior/blocks/fluid/liquid_block.rs index 787c3fe5805..e3badc828b0 100644 --- a/steel-core/src/behavior/blocks/fluid/liquid_block.rs +++ b/steel-core/src/behavior/blocks/fluid/liquid_block.rs @@ -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 { Some(self.block.default_state()) } @@ -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()); + } } diff --git a/steel-core/src/behavior/item.rs b/steel-core/src/behavior/item.rs index 92d45f6b7c7..5da9c07b5db 100644 --- a/steel-core/src/behavior/item.rs +++ b/steel-core/src/behavior/item.rs @@ -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; @@ -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 { diff --git a/steel-core/src/behavior/items/mod.rs b/steel-core/src/behavior/items/mod.rs index a4a7f39775e..8e40994a5dd 100644 --- a/steel-core/src/behavior/items/mod.rs +++ b/steel-core/src/behavior/items/mod.rs @@ -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; @@ -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}; diff --git a/steel-core/src/behavior/items/spawn_egg.rs b/steel-core/src/behavior/items/spawn_egg.rs new file mode 100644 index 00000000000..f4cf3b73e5a --- /dev/null +++ b/steel-core/src/behavior/items/spawn_egg.rs @@ -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 { + stack.get(ENTITY_DATA).map(EntityData::entity_type) + } + + fn spawn_mob( + world: &Arc, + 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( + 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(stack: &ItemStack, parent: &M) -> Option { + 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() { + 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) + } +} + +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 { + // 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; + } + + 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 + } +} diff --git a/steel-core/src/behavior/mod.rs b/steel-core/src/behavior/mod.rs index 60bc98f8951..e41f2da78bd 100644 --- a/steel-core/src/behavior/mod.rs +++ b/steel-core/src/behavior/mod.rs @@ -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; } @@ -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); diff --git a/steel-core/src/chunk_saver/storage/entities.rs b/steel-core/src/chunk_saver/storage/entities.rs index ae005074afa..336c2884d4c 100644 --- a/steel-core/src/chunk_saver/storage/entities.rs +++ b/steel-core/src/chunk_saver/storage/entities.rs @@ -1,4 +1,5 @@ use super::*; +use crate::entity::clamp_loaded_entity_position; impl ChunkStorage { pub(super) fn entities_to_persistent(entities: &[SharedEntity]) -> Vec { @@ -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, @@ -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); diff --git a/steel-core/src/chunk_saver/storage/mod.rs b/steel-core/src/chunk_saver/storage/mod.rs index 77181debc21..66af96aa5e7 100644 --- a/steel-core/src/chunk_saver/storage/mod.rs +++ b/steel-core/src/chunk_saver/storage/mod.rs @@ -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` 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) -> i8 { diff --git a/steel-core/src/chunk_saver/storage/tests/entities.rs b/steel-core/src/chunk_saver/storage/tests/entities.rs index a5b1d417a1e..049cfb2a0b9 100644 --- a/steel-core/src/chunk_saver/storage/tests/entities.rs +++ b/steel-core/src/chunk_saver/storage/tests/entities.rs @@ -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 { @@ -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] diff --git a/steel-core/src/entity/entities/mobs/passive/chicken/mod.rs b/steel-core/src/entity/entities/mobs/passive/chicken/mod.rs index 3da84b9462d..9e4bc87c56c 100644 --- a/steel-core/src/entity/entities/mobs/passive/chicken/mod.rs +++ b/steel-core/src/entity/entities/mobs/passive/chicken/mod.rs @@ -10,6 +10,7 @@ use simdnbt::owned::NbtCompound; use steel_macros::entity_behavior; use steel_registry::chicken_sound_variant::{ChickenAge, ChickenSoundVariantRef}; use steel_registry::chicken_variant::ChickenVariantRef; +use steel_registry::data_components::vanilla_components::{CHICKEN_SOUND_VARIANT, CHICKEN_VARIANT}; use steel_registry::entity_type::{ EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef, }; @@ -404,6 +405,15 @@ impl Entity for ChickenEntity { self.entity_type } + fn apply_implicit_item_components(&self, item_stack: &ItemStack) { + if let Some(variant) = item_stack.get(CHICKEN_VARIANT) { + self.set_variant(variant.value()); + } + if let Some(sound_variant) = item_stack.get(CHICKEN_SOUND_VARIANT) { + self.set_sound_variant(sound_variant.value()); + } + } + fn base_tick(&self) { Mob::base_tick_mob(self); } diff --git a/steel-core/src/entity/entities/mobs/passive/cow/mod.rs b/steel-core/src/entity/entities/mobs/passive/cow/mod.rs index b4f50d73ddf..b854dbd0f4e 100644 --- a/steel-core/src/entity/entities/mobs/passive/cow/mod.rs +++ b/steel-core/src/entity/entities/mobs/passive/cow/mod.rs @@ -10,6 +10,7 @@ use steel_macros::entity_behavior; use steel_protocol::packets::game::SoundSource; use steel_registry::cow_sound_variant::CowSoundVariantRef; use steel_registry::cow_variant::CowVariantRef; +use steel_registry::data_components::vanilla_components::{COW_SOUND_VARIANT, COW_VARIANT}; use steel_registry::entity_type::{ EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef, }; @@ -247,6 +248,15 @@ impl Entity for CowEntity { self.entity_type } + fn apply_implicit_item_components(&self, item_stack: &ItemStack) { + if let Some(variant) = item_stack.get(COW_VARIANT) { + self.set_variant(variant.value()); + } + if let Some(sound_variant) = item_stack.get(COW_SOUND_VARIANT) { + self.set_sound_variant(sound_variant.value()); + } + } + fn base_tick(&self) { Mob::base_tick_mob(self); } diff --git a/steel-core/src/entity/entities/mobs/passive/pig/mod.rs b/steel-core/src/entity/entities/mobs/passive/pig/mod.rs index 44baf43029c..5376b5e93c8 100644 --- a/steel-core/src/entity/entities/mobs/passive/pig/mod.rs +++ b/steel-core/src/entity/entities/mobs/passive/pig/mod.rs @@ -9,6 +9,7 @@ use glam::DVec3; use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView; use simdnbt::owned::NbtCompound; use steel_macros::entity_behavior; +use steel_registry::data_components::vanilla_components::{PIG_SOUND_VARIANT, PIG_VARIANT}; use steel_registry::entity_type::{ EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef, }; @@ -237,6 +238,15 @@ impl Entity for PigEntity { self.entity_type } + fn apply_implicit_item_components(&self, item_stack: &ItemStack) { + if let Some(variant) = item_stack.get(PIG_VARIANT) { + self.set_variant(variant.value()); + } + if let Some(sound_variant) = item_stack.get(PIG_SOUND_VARIANT) { + self.set_sound_variant(sound_variant.value()); + } + } + fn base_tick(&self) { Mob::base_tick_mob(self); } diff --git a/steel-core/src/entity/entities/mobs/passive/sheep/mod.rs b/steel-core/src/entity/entities/mobs/passive/sheep/mod.rs index 75bdfdf3f64..50c6b1475e9 100644 --- a/steel-core/src/entity/entities/mobs/passive/sheep/mod.rs +++ b/steel-core/src/entity/entities/mobs/passive/sheep/mod.rs @@ -7,7 +7,7 @@ use simdnbt::owned::NbtCompound; use steel_macros::entity_behavior; use steel_protocol::packets::game::SoundSource; use steel_registry::biome::BiomeRef; -use steel_registry::data_components::vanilla_components::DYE; +use steel_registry::data_components::vanilla_components::{DYE, SHEEP_COLOR}; use steel_registry::entity_type::{ EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef, }; @@ -369,6 +369,12 @@ impl Entity for SheepEntity { self.entity_type } + fn apply_implicit_item_components(&self, item_stack: &ItemStack) { + if let Some(color) = item_stack.get(SHEEP_COLOR) { + self.set_color(*color); + } + } + fn base_tick(&self) { Mob::base_tick_mob(self); } diff --git a/steel-core/src/entity/entity/mod.rs b/steel-core/src/entity/entity/mod.rs index c49b944a499..9b2d277a8e8 100644 --- a/steel-core/src/entity/entity/mod.rs +++ b/steel-core/src/entity/entity/mod.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeSet; + use super::*; use crate::entity::leash::Leashable; use steel_math::DEGREE_90; @@ -8,6 +10,35 @@ const FUDGE_SMALL_DIMENSION_LIMIT: f32 = 4.0; /// Vanilla `Entity.fudgePositionAfterSizeChange` epsilon padding (vanilla `1.0E-6`). const FUDGE_POSITION_EPSILON: f64 = 1.0e-6; +const MAX_ENTITY_MOTION_COMPONENT: f64 = 10.0; + +fn read_nbt_dvec3(nbt: &BorrowedNbtCompoundView<'_, '_>, key: &str) -> Option { + let values = nbt.list(key)?.doubles()?; + let &[x, y, z, ..] = values.as_slice() else { + return None; + }; + Some(DVec3::new(x, y, z)) +} + +fn read_nbt_rotation(nbt: &BorrowedNbtCompoundView<'_, '_>, key: &str) -> Option<(f32, f32)> { + let values = nbt.list(key)?.floats()?; + let &[yaw, pitch, ..] = values.as_slice() else { + return None; + }; + Some((yaw, pitch)) +} + +fn sanitize_nbt_motion(motion: DVec3) -> DVec3 { + let sanitize = |value: f64| { + if value.abs() > MAX_ENTITY_MOTION_COMPONENT { + 0.0 + } else { + value + } + }; + DVec3::new(sanitize(motion.x), sanitize(motion.y), sanitize(motion.z)) +} + /// Final state accepted from a client-authored movement packet. #[derive(Debug, Clone, Copy, PartialEq)] pub struct AcceptedClientMovement { @@ -3565,6 +3596,109 @@ pub trait Entity: EntityEventSource + ErasedType + Send + Sync + 'static { /// Mirrors vanilla's `Entity.readAdditionalSaveData()`. fn load_additional(&self, _nbt: BorrowedNbtCompoundView<'_, '_>) {} + /// Applies entity-specific implicit components carried by an item stack. + /// + /// Mirrors the overridable part of vanilla's `Entity.applyImplicitComponents`. + fn apply_implicit_item_components(&self, _item_stack: &ItemStack) {} + + /// Applies the merged entity data used by vanilla `TypedEntityData.loadInto`. + /// + /// Spawn-item data is merged into the entity's current save state before it + /// is loaded. Keeping that merge at the entity boundary preserves defaults + /// and entity-specific state when the component only overrides one field. + fn apply_spawn_data(&self, nbt: BorrowedNbtCompoundView<'_, '_>) { + let read_int = |key: &str| { + nbt.int(key) + .or_else(|| nbt.short(key).map(i32::from)) + .or_else(|| nbt.byte(key).map(i32::from)) + }; + + if let Some(position) = read_nbt_dvec3(&nbt, "Pos") + && position.is_finite() + { + self.base() + .set_position_local(super::clamp_loaded_entity_position(position)); + } + + if let Some(motion) = read_nbt_dvec3(&nbt, "Motion") { + self.set_velocity(sanitize_nbt_motion(motion)); + } + + if let Some((yaw, pitch)) = read_nbt_rotation(&nbt, "Rotation") { + self.set_rotation((yaw, pitch)); + if let Some(living) = self.as_living_entity() { + living.set_y_head_rot(yaw); + living.set_y_body_rot(yaw); + } + } + + if let Some(fall_distance) = nbt + .double("fall_distance") + .or_else(|| nbt.double("FallDistance")) + { + self.set_fall_distance(fall_distance); + } + + if let Some(on_ground) = nbt.byte("OnGround") { + self.set_on_ground(on_ground != 0); + } + + let mut save_data = self.base().save_data(); + if let Some(air_supply) = read_int("Air") { + save_data.air_supply = air_supply; + } + if let Some(portal_cooldown) = read_int("PortalCooldown") { + save_data.portal_cooldown = portal_cooldown; + } + if let Some(no_gravity) = nbt.byte("NoGravity") { + save_data.no_gravity = no_gravity != 0; + } + if let Some(invulnerable) = nbt.byte("Invulnerable") { + save_data.invulnerable = invulnerable != 0; + } + if let Some(custom_name) = nbt + .get("CustomName") + .and_then(|tag| TextComponent::from_nbt(&tag.to_owned())) + { + save_data.custom_name = Some(custom_name); + } + if let Some(custom_name_visible) = nbt.byte("CustomNameVisible") { + save_data.custom_name_visible = custom_name_visible != 0; + } + if let Some(silent) = nbt.byte("Silent") { + save_data.silent = silent != 0; + } + if let Some(glowing) = nbt.byte("Glowing") { + save_data.glowing = glowing != 0; + } + if let Some(tags) = nbt.list("Tags").and_then(|list| list.strings()) { + save_data.tags = tags + .iter() + .take(MAX_ENTITY_TAGS) + .map(|tag| tag.to_str().into_owned()) + .collect::>(); + } + if let Some(custom_data) = nbt.compound("data") { + save_data.custom_data = custom_data.to_owned(); + } + self.base().replace_save_data(save_data); + + if let Some(remaining_fire_ticks) = read_int("Fire") { + self.set_remaining_fire_ticks(remaining_fire_ticks); + } + if let Some(ticks_frozen) = read_int("TicksFrozen") { + self.set_ticks_frozen(ticks_frozen); + } + if let Some(has_visual_fire) = nbt.byte("HasVisualFire") { + self.base().set_visual_fire(has_visual_fire != 0); + } + + self.load_additional(nbt); + self.set_old_position_to_current(); + self.base().set_old_rotation_to_current(); + self.sync_base_entity_data(); + } + /// Returns vanilla `Entity.isInvulnerableToBase`. fn is_invulnerable_to_base(&self, source: &DamageSource) -> bool { self.is_removed() diff --git a/steel-core/src/entity/mob/mod.rs b/steel-core/src/entity/mob/mod.rs index 324e56d7f91..4c996ac0118 100644 --- a/steel-core/src/entity/mob/mod.rs +++ b/steel-core/src/entity/mob/mod.rs @@ -26,12 +26,13 @@ use steel_registry::sound_event::SoundEventRef; use steel_registry::vanilla_block_tags::BlockTag; use steel_registry::{ REGISTRY, RegistryExt, TaggedRegistryExt, vanilla_attributes, vanilla_damage_types, - vanilla_entities, vanilla_game_events, vanilla_game_rules, + vanilla_entities, vanilla_game_events, vanilla_game_rules, vanilla_items, }; use steel_utils::locks::SyncMutex; use steel_utils::types::{Difficulty, InteractionHand}; use steel_utils::{BlockPos, ChunkPos, Downcast as _, Identifier, WorldAabb, axis::Axis}; +use crate::behavior::items::SpawnEggItem; use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext, ITEM_BEHAVIORS, InteractionResult}; use crate::enchantment_helper::{self, EnchantmentDamageContext, EnchantmentPostAttackContext}; use crate::entity::ai::control::{ @@ -52,6 +53,7 @@ use crate::entity::{ use crate::inventory::equipment::EquipmentSlot; use crate::physics::MoveResult; use crate::player::Player; +use crate::world::game_event::GameEventContext; use crate::world::{LevelReader, World}; const MOB_FLAG_NO_AI: i8 = 1; @@ -503,7 +505,18 @@ pub trait Mob: LivingEntity + Leashable { return InteractionResult::Pass; } - // TODO: Handle name tags and spawn eggs once item-on-entity behavior exists. + let important_interaction = self.check_and_handle_important_interactions(player, hand); + if important_interaction.consumes_action() { + if let Some(world) = self.level() { + world.game_event( + &vanilla_game_events::ENTITY_INTERACT, + self.block_position(), + &GameEventContext::new(Some(player), None), + ); + } + return important_interaction; + } + let interaction_result = self.interact_entity(player, hand, location); if interaction_result != InteractionResult::Pass { return interaction_result; @@ -517,6 +530,51 @@ pub trait Mob: LivingEntity + Leashable { interaction_result } + /// Handles vanilla `Mob.checkAndHandleImportantInteractions`. + fn check_and_handle_important_interactions( + &self, + player: &Player, + hand: InteractionHand, + ) -> InteractionResult { + let Some(living_entity) = self.as_living_entity() else { + return InteractionResult::Pass; + }; + + let item = { + let inventory = player.inventory.lock(); + inventory.get_item_in_hand(hand).item() + }; + + if item.key == vanilla_items::NAME_TAG.key { + let name_tag_result = { + let mut inventory = player.inventory.lock(); + let item_stack = inventory.get_item_in_hand_mut(hand); + ITEM_BEHAVIORS.get_behavior(item).interact_living_entity( + item_stack, + player, + living_entity, + hand, + ) + }; + if name_tag_result.consumes_action() { + return name_tag_result; + } + } + + if ITEM_BEHAVIORS.get_behavior(item).as_spawn_egg().is_some() { + let spawn_egg_result = { + let mut inventory = player.inventory.lock(); + let item_stack = inventory.get_item_in_hand_mut(hand); + SpawnEggItem::interact_with_mob(item_stack, player, self) + }; + if spawn_egg_result.consumes_action() { + return spawn_egg_result; + } + } + + InteractionResult::Pass + } + /// Handles vanilla `Mob.mobInteract`. fn mob_interact(&self, _player: &Player, _hand: InteractionHand) -> InteractionResult { InteractionResult::Pass diff --git a/steel-core/src/entity/mod.rs b/steel-core/src/entity/mod.rs index 7e90213fe57..2c11da59c7b 100644 --- a/steel-core/src/entity/mod.rs +++ b/steel-core/src/entity/mod.rs @@ -78,6 +78,27 @@ use crate::{enchantment_helper, entity::damage::DamageSource, player::Player}; use entities::ExperienceOrbEntity; +pub(crate) const ENTITY_LOAD_MAX_HORIZONTAL_POSITION: f64 = 3.000_051_2E7; +pub(crate) const ENTITY_LOAD_MAX_VERTICAL_POSITION: f64 = 2.0E7; + +/// Clamps an entity position using vanilla's load-time world-bound limits. +pub(crate) 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, + ), + ) +} + fn nbt_bool(value: bool) -> NbtTag { NbtTag::Byte(i8::from(value)) } @@ -820,7 +841,11 @@ pub use projectile::{ compute_margin, get_hit_result_on_view_vector, spawn_throwable_item_projectile, }; pub use registry::{ENTITIES, EntityLoadRequest, EntityRegistry, init_entities}; -pub(crate) use spawn::{AgeableMobGroupData, EntitySpawnReason, SpawnGroupData}; +pub(crate) use spawn::{ + AgeableMobGroupData, EntitySpawnPlacement, EntitySpawnReason, EntitySpawnRequest, + SpawnGroupData, add_spawned_entity, apply_implicit_item_stack_components, + create_entity_instance, spawn_entity, +}; pub(crate) use storage::{EntityStorage, EntityStorageAddResult}; pub use synced_data::{EntitySyncedData, LivingEntitySyncedData}; pub(crate) use ticking::{ diff --git a/steel-core/src/entity/registry.rs b/steel-core/src/entity/registry.rs index 4b43c8f73ea..83f52767669 100644 --- a/steel-core/src/entity/registry.rs +++ b/steel-core/src/entity/registry.rs @@ -103,6 +103,8 @@ impl EntityRegistry { let nbt: BorrowedNbtCompoundView<'_, '_> = nbt.into(); entity.load_additional(nbt); + entity.set_old_position_to_current(); + entity.base().set_old_rotation_to_current(); entity.sync_base_entity_data(); } @@ -188,6 +190,8 @@ impl EntityRegistry { let entity: SharedEntity = Arc::new(RawEntity::from_saved(load, entity_type)); let nbt: BorrowedNbtCompoundView<'_, '_> = nbt.into(); entity.load_additional(nbt); + entity.set_old_position_to_current(); + entity.base().set_old_rotation_to_current(); entity } diff --git a/steel-core/src/entity/spawn.rs b/steel-core/src/entity/spawn.rs index fc7888d17eb..93ae077a3ee 100644 --- a/steel-core/src/entity/spawn.rs +++ b/steel-core/src/entity/spawn.rs @@ -1,3 +1,18 @@ +use std::{io::Cursor, sync::Arc}; + +use glam::DVec3; +use simdnbt::borrow::read_compound; +use steel_math::{DEGREE_360, wrap_degrees}; +use steel_registry::data_components::vanilla_components::{CUSTOM_DATA, CUSTOM_NAME, ENTITY_DATA}; +use steel_registry::entity_type::EntityTypeRef; +use steel_registry::item_stack::ItemStack; +use steel_utils::nbt::merge_nbt_compounds; +use steel_utils::{BlockPos, WorldAabb, axis::Axis, types::Difficulty}; + +use super::{AddEntityError, ENTITIES, SharedEntity, next_entity_id}; +use crate::physics::{CollisionWorld, WorldCollisionProvider, collide}; +use crate::world::World; + /// Vanilla `EntitySpawnReason`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EntitySpawnReason { @@ -34,6 +49,229 @@ impl EntitySpawnReason { } } +/// Placement modes used by the shared vanilla entity-spawn coordinator. +#[derive(Debug, Clone, Copy)] +pub(crate) enum EntitySpawnPlacement { + /// Spawn at a block center, optionally applying vanilla's downward offset. + Block { + pos: BlockPos, + try_move_down: bool, + moved_up: bool, + }, +} + +impl EntitySpawnPlacement { + fn factory_position(self) -> DVec3 { + match self { + Self::Block { + pos, + try_move_down: true, + .. + } => DVec3::new( + f64::from(pos.x()) + 0.5, + f64::from(pos.y()) + 1.0, + f64::from(pos.z()) + 0.5, + ), + Self::Block { pos, .. } => DVec3::new( + f64::from(pos.x()) + 0.5, + f64::from(pos.y()), + f64::from(pos.z()) + 0.5, + ), + } + } +} + +/// Request for a complete, world-inserting entity spawn. +#[derive(Debug, Clone, Copy)] +pub(crate) struct EntitySpawnRequest<'a> { + pub entity_type: EntityTypeRef, + pub placement: EntitySpawnPlacement, + pub reason: EntitySpawnReason, + pub finalize_spawn: bool, + pub play_ambient_sound: bool, + pub item_stack: Option<&'a ItemStack>, + pub user_is_operator: bool, +} + +/// Failure from the shared spawn coordinator. +#[derive(Debug)] +pub(crate) enum EntitySpawnError { + InvalidPosition, + Peaceful, + MissingFactory, + InvalidEntityData, + AddEntity, +} + +/// Creates an entity instance through the generated entity factory registry. +pub(crate) fn create_entity_instance( + world: &Arc, + entity_type: EntityTypeRef, + position: DVec3, +) -> Result { + if !position.is_finite() || !World::is_in_spawnable_bounds(BlockPos::from(position)) { + return Err(EntitySpawnError::InvalidPosition); + } + + if world.difficulty() == Difficulty::Peaceful && !entity_type.allowed_in_peaceful { + return Err(EntitySpawnError::Peaceful); + } + + if !ENTITIES.has_factory(entity_type) { + return Err(EntitySpawnError::MissingFactory); + } + + ENTITIES + .create( + entity_type, + next_entity_id(), + position, + Arc::downgrade(world), + ) + .ok_or(EntitySpawnError::MissingFactory) +} + +/// Inserts a fully initialized entity into the live world entity manager. +pub(crate) fn add_spawned_entity( + world: &Arc, + entity: SharedEntity, +) -> Result<(), AddEntityError> { + world.try_add_entity(entity) +} + +/// Applies the implicit entity data carried by an item stack. +pub(crate) fn apply_implicit_item_stack_components(entity: &SharedEntity, item_stack: &ItemStack) { + entity.apply_implicit_item_components(item_stack); + + if let Some(custom_name) = item_stack.get(CUSTOM_NAME) { + entity.set_custom_name(Some(custom_name.clone())); + } + + if let Some(custom_data) = item_stack.get(CUSTOM_DATA) { + entity.set_custom_data(custom_data.copy_tag()); + } +} + +/// Applies all entity data carried by an item stack during a normal item spawn. +pub(crate) fn apply_item_stack_components( + entity: &SharedEntity, + item_stack: &ItemStack, + user_is_operator: bool, +) -> Result<(), EntitySpawnError> { + apply_implicit_item_stack_components(entity, item_stack); + + let Some(entity_data) = item_stack.get(ENTITY_DATA) else { + return Ok(()); + }; + + if entity_data.entity_type() != entity.entity_type() { + return Ok(()); + } + + if entity.entity_type().only_op_can_set_nbt && !user_is_operator { + return Ok(()); + } + + let mut merged = entity.nbt_for_data_compare(); + merge_nbt_compounds(&mut merged, &entity_data.data().copy_tag()); + + let mut bytes = Vec::new(); + merged.write(&mut bytes); + let mut cursor = Cursor::new(bytes.as_slice()); + let borrowed = read_compound(&mut cursor).map_err(|_| EntitySpawnError::InvalidEntityData)?; + entity.apply_spawn_data((&borrowed).into()); + Ok(()) +} + +/// Mirrors vanilla `EntityType.spawn` for server-side entity creation. +pub(crate) fn spawn_entity( + world: &Arc, + request: EntitySpawnRequest<'_>, +) -> Result { + let entity = create_entity_instance( + world, + request.entity_type, + request.placement.factory_position(), + )?; + + let EntitySpawnPlacement::Block { + pos, + try_move_down, + moved_up, + } = request.placement; + let position_above = DVec3::new( + f64::from(pos.x()) + 0.5, + f64::from(pos.y()) + 1.0, + f64::from(pos.z()) + 0.5, + ); + if try_move_down { + entity.base().set_position_local(position_above); + } + + let y_offset = if try_move_down { + entity_y_offset(world, pos, moved_up, entity.bounding_box()) + } else { + 0.0 + }; + let position = DVec3::new( + f64::from(pos.x()) + 0.5, + f64::from(pos.y()) + y_offset, + f64::from(pos.z()) + 0.5, + ); + let rotation = (wrap_degrees(rand::random::() * DEGREE_360), 0.0); + + entity.base().set_position_local(position); + entity.set_rotation(rotation); + entity.set_old_position_to_current(); + entity.base().set_old_rotation_to_current(); + + if request.finalize_spawn + && let Some(mob) = entity.as_mob() + { + mob.set_y_head_rot(rotation.0); + mob.set_y_body_rot(rotation.0); + let _ = mob.finalize_spawn(world, request.reason, None); + } + + if let Some(item_stack) = request.item_stack { + apply_item_stack_components(&entity, item_stack, request.user_is_operator)?; + } + + add_spawned_entity(world, Arc::clone(&entity)).map_err(|_| EntitySpawnError::AddEntity)?; + + if request.play_ambient_sound + && let Some(mob) = entity.as_mob() + { + mob.play_ambient_sound(); + } + + Ok(entity) +} + +fn entity_y_offset( + world: &Arc, + spawn_pos: BlockPos, + moved_up: bool, + entity_box: WorldAabb, +) -> f64 { + let min_y = f64::from(spawn_pos.y()) - if moved_up { 1.0 } else { 0.0 }; + let collision_box = WorldAabb::new( + f64::from(spawn_pos.x()), + min_y, + f64::from(spawn_pos.z()), + f64::from(spawn_pos.x() + 1), + f64::from(spawn_pos.y() + 1), + f64::from(spawn_pos.z() + 1), + ); + let shapes = WorldCollisionProvider::new(world).get_block_collisions(&collision_box); + 1.0 + collide( + Axis::Y, + &entity_box, + &shapes, + if moved_up { -2.0 } else { -1.0 }, + ) +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum SpawnGroupData { AgeableMob(AgeableMobGroupData), @@ -101,7 +339,29 @@ impl AgeableMobGroupData { #[cfg(test)] mod tests { - use super::AgeableMobGroupData; + use std::sync::{Arc, Weak}; + + use glam::DVec3; + use simdnbt::owned::NbtCompound; + use steel_registry::data_components::CustomData; + use steel_registry::data_components::components::EntityData; + use steel_registry::data_components::vanilla_components::{ + CHICKEN_SOUND_VARIANT, CHICKEN_VARIANT, COW_SOUND_VARIANT, COW_VARIANT, ENTITY_DATA, + PIG_VARIANT, SHEEP_COLOR, + }; + use steel_registry::init_vanilla_registry; + use steel_registry::item_stack::ItemStack; + use steel_registry::{ + DyeColor, RegistryReference, vanilla_chicken_sound_variants, vanilla_chicken_variants, + vanilla_cow_sound_variants, vanilla_cow_variants, vanilla_entities, vanilla_items, + vanilla_pig_variants, + }; + use text_components::TextComponent; + + use crate::entity::entities::{ChickenEntity, CowEntity, PigEntity, SheepEntity}; + use crate::entity::{AgeableMob, Entity, SharedEntity}; + + use super::{AgeableMobGroupData, apply_item_stack_components}; #[test] fn ageable_group_data_increments_before_later_baby_rolls_can_apply() { @@ -130,4 +390,146 @@ mod tests { ); assert_eq!(group_data.group_size(), 2); } + + #[test] + fn item_entity_data_overrides_entity_state_without_replacing_defaults() { + init_vanilla_registry(); + + let pig = Arc::new(PigEntity::new( + &vanilla_entities::PIG, + 1, + DVec3::ZERO, + Weak::new(), + )); + let entity: SharedEntity = pig.clone(); + pig.set_custom_name(Some(TextComponent::plain("Existing"))); + + let mut payload = NbtCompound::new(); + payload.insert("Age", pig.get_baby_start_age()); + payload.insert("variant", vanilla_pig_variants::WARM.key.to_string()); + let entity_data = EntityData::new( + &vanilla_entities::PIG, + CustomData::try_from_compound(payload).expect("test entity data should be valid"), + ); + let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG); + spawn_egg.set(ENTITY_DATA, entity_data); + + apply_item_stack_components(&entity, &spawn_egg, false) + .expect("valid typed entity data should load"); + + assert_eq!(pig.get_age(), pig.get_baby_start_age()); + assert_eq!(pig.variant().key, vanilla_pig_variants::WARM.key); + assert!(AgeableMob::is_baby(pig.as_ref())); + assert_eq!(pig.custom_name(), Some(TextComponent::plain("Existing"))); + } + + #[test] + fn item_pig_variant_overrides_spawn_variant() { + init_vanilla_registry(); + + let pig = Arc::new(PigEntity::new( + &vanilla_entities::PIG, + 1, + DVec3::ZERO, + Weak::new(), + )); + pig.set_variant(&vanilla_pig_variants::WARM); + let entity: SharedEntity = pig.clone(); + + let mut spawn_egg = ItemStack::new(&vanilla_items::PIG_SPAWN_EGG); + spawn_egg.set( + PIG_VARIANT, + RegistryReference::new(&vanilla_pig_variants::COLD), + ); + + apply_item_stack_components(&entity, &spawn_egg, false) + .expect("valid pig variant component should apply"); + + assert_eq!(pig.variant().key, vanilla_pig_variants::COLD.key); + } + + #[test] + fn item_cow_components_override_spawn_state() { + init_vanilla_registry(); + + let cow = Arc::new(CowEntity::new( + &vanilla_entities::COW, + 1, + DVec3::ZERO, + Weak::new(), + )); + let entity: SharedEntity = cow.clone(); + + let mut spawn_egg = ItemStack::new(&vanilla_items::COW_SPAWN_EGG); + spawn_egg.set( + COW_VARIANT, + RegistryReference::new(&vanilla_cow_variants::COLD), + ); + spawn_egg.set( + COW_SOUND_VARIANT, + RegistryReference::new(&vanilla_cow_sound_variants::MOODY), + ); + + apply_item_stack_components(&entity, &spawn_egg, false) + .expect("valid cow components should apply"); + + assert_eq!(cow.variant().key, vanilla_cow_variants::COLD.key); + assert_eq!( + cow.sound_variant().key, + vanilla_cow_sound_variants::MOODY.key + ); + } + + #[test] + fn item_chicken_components_override_spawn_state() { + init_vanilla_registry(); + + let chicken = Arc::new(ChickenEntity::new( + &vanilla_entities::CHICKEN, + 1, + DVec3::ZERO, + Weak::new(), + )); + let entity: SharedEntity = chicken.clone(); + + let mut spawn_egg = ItemStack::new(&vanilla_items::CHICKEN_SPAWN_EGG); + spawn_egg.set( + CHICKEN_VARIANT, + RegistryReference::new(&vanilla_chicken_variants::COLD), + ); + spawn_egg.set( + CHICKEN_SOUND_VARIANT, + RegistryReference::new(&vanilla_chicken_sound_variants::PICKY), + ); + + apply_item_stack_components(&entity, &spawn_egg, false) + .expect("valid chicken components should apply"); + + assert_eq!(chicken.variant().key, vanilla_chicken_variants::COLD.key); + assert_eq!( + chicken.sound_variant().key, + vanilla_chicken_sound_variants::PICKY.key + ); + } + + #[test] + fn item_sheep_color_overrides_spawn_state() { + init_vanilla_registry(); + + let sheep = Arc::new(SheepEntity::new( + &vanilla_entities::SHEEP, + 1, + DVec3::ZERO, + Weak::new(), + )); + let entity: SharedEntity = sheep.clone(); + + let mut spawn_egg = ItemStack::new(&vanilla_items::SHEEP_SPAWN_EGG); + spawn_egg.set(SHEEP_COLOR, DyeColor::Pink); + + apply_item_stack_components(&entity, &spawn_egg, false) + .expect("valid sheep color should apply"); + + assert_eq!(sheep.color(), DyeColor::Pink); + } } diff --git a/steel-core/src/entity/tests/mod.rs b/steel-core/src/entity/tests/mod.rs index 2b6f1717b64..bdd41e39ab0 100644 --- a/steel-core/src/entity/tests/mod.rs +++ b/steel-core/src/entity/tests/mod.rs @@ -1,6 +1,10 @@ -use std::sync::{Arc, Weak}; +use std::{ + io::Cursor, + sync::{Arc, Weak}, +}; use glam::DVec3; +use simdnbt::borrow::read_compound; use simdnbt::owned::{NbtCompound, NbtList, NbtTag}; use steel_protocol::packets::game::RelativeMovement; use steel_registry::blocks::{ @@ -44,14 +48,15 @@ use crate::world::{LevelReader, World}; use super::{ ActiveMobEffect, AttributeModifier, AttributeModifierOperation, DAMAGE_KNOCKBACK_POWER, - DEFAULT_SWING_DURATION, DEFAULT_TICKS_REQUIRED_TO_FREEZE, Entity, EntityBase, - EntityFluidContact, EntityLevelCallback, EntityMoveError, EntityOwnership, EntitySyncedData, - EntityVerticalMovementStateUpdate, InsideBlockEffectCollector, InsideBlockEffectType, - LivingEntity, LivingEntityBase, LivingTravelInput, MobEffectInstance, RemovalReason, - SPEED_MODIFIER_POWDER_SNOW_ID, SharedEntity, block_state_suffocates_eye_box, - closest_open_space_direction, fall_damage_reset_clip_target, fall_flying_collision_damage, - fall_flying_free_fall_interval, get_input_vector, indirect_passengers, - passenger_transition_position, passenger_transition_rotation, remove_after_changing_dimensions, + DEFAULT_SWING_DURATION, DEFAULT_TICKS_REQUIRED_TO_FREEZE, ENTITY_LOAD_MAX_HORIZONTAL_POSITION, + ENTITY_LOAD_MAX_VERTICAL_POSITION, Entity, EntityBase, EntityFluidContact, EntityLevelCallback, + EntityMoveError, EntityOwnership, EntitySyncedData, EntityVerticalMovementStateUpdate, + InsideBlockEffectCollector, InsideBlockEffectType, LivingEntity, LivingEntityBase, + LivingTravelInput, MobEffectInstance, RemovalReason, SPEED_MODIFIER_POWDER_SNOW_ID, + SharedEntity, block_state_suffocates_eye_box, closest_open_space_direction, + fall_damage_reset_clip_target, fall_flying_collision_damage, fall_flying_free_fall_interval, + get_input_vector, indirect_passengers, passenger_transition_position, + passenger_transition_rotation, remove_after_changing_dimensions, should_apply_entity_cramming_damage, should_apply_resolved_movement, start_riding_entities, transfer_leashables_to_holder, trapdoor_usable_as_ladder_state, }; @@ -214,6 +219,33 @@ fn command_data_compare_nbt_contains_base_and_custom_data() { )); } +#[test] +fn spawn_data_clamps_position_and_refreshes_old_transform() { + let entity = TypedTestEntity::new(1, &vanilla_entities::PIG); + let mut nbt = NbtCompound::new(); + nbt.insert( + "Pos", + NbtList::Double(vec![100_000_000.0, -100_000_000.0, 100_000_000.0]), + ); + nbt.insert("Rotation", NbtList::Float(vec![90.0, -30.0])); + + let mut bytes = Vec::new(); + nbt.write(&mut bytes); + let borrowed = + read_compound(&mut Cursor::new(bytes.as_slice())).expect("test spawn data should be valid"); + entity.apply_spawn_data((&borrowed).into()); + + let expected_position = DVec3::new( + ENTITY_LOAD_MAX_HORIZONTAL_POSITION, + -ENTITY_LOAD_MAX_VERTICAL_POSITION, + ENTITY_LOAD_MAX_HORIZONTAL_POSITION, + ); + assert_eq!(entity.position(), expected_position); + assert_eq!(entity.base().old_position(), expected_position); + assert_eq!(entity.rotation(), (90.0, -30.0)); + assert_eq!(entity.base().old_rotation(), (90.0, -30.0)); +} + #[test] fn command_data_compare_nbt_contains_implemented_living_data() { init_vanilla_registry(); diff --git a/steel-core/src/player/game_mode/block_breaking.rs b/steel-core/src/player/game_mode/block_breaking.rs index c83141a28f9..4219d33df42 100644 --- a/steel-core/src/player/game_mode/block_breaking.rs +++ b/steel-core/src/player/game_mode/block_breaking.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use steel_protocol::packets::game::CBlockUpdate; use steel_registry::blocks::block_state_ext::BlockStateExt; use steel_registry::data_components::AdventureModePredicate; -use steel_registry::data_components::vanilla_components::CAN_BREAK; +use steel_registry::data_components::vanilla_components::{CAN_BREAK, CAN_PLACE_ON}; use steel_registry::equipment::EquipmentSlot; use steel_registry::stat::vanilla_stat_types; use steel_registry::vanilla_attributes; @@ -30,6 +30,25 @@ use crate::player::food_data::food_constants; use crate::world::{ConditionalBlockSetResult, World, game_event::GameEventContext}; impl Player { + /// Mirrors vanilla `Player.mayUseItemAt` for adventure-mode item use. + pub(crate) fn may_use_item_at( + &self, + pos: BlockPos, + direction: Direction, + item_stack: &ItemStack, + ) -> bool { + if self.abilities.lock().may_build { + return true; + } + + let Some(can_place_on) = item_stack.get(CAN_PLACE_ON) else { + return false; + }; + let target = pos.relative(direction.opposite()); + let world = self.get_world(); + Self::matches_adventure_mode_predicate(can_place_on, &world, target) + } + /// Mirrors vanilla `Player.blockActionRestricted` for block breaking. pub(super) fn block_action_restricted(&self, world: &World, pos: BlockPos) -> bool { let game_mode = self.game_mode(); @@ -57,10 +76,10 @@ impl Player { let Some(can_break) = can_break else { return true; }; - !Self::can_break_block_in_adventure_mode(&can_break, world, pos) + !Self::matches_adventure_mode_predicate(&can_break, world, pos) } - fn can_break_block_in_adventure_mode( + fn matches_adventure_mode_predicate( predicate: &AdventureModePredicate, world: &World, pos: BlockPos, diff --git a/steel-registry/build/entities.rs b/steel-registry/build/entities.rs index 7e218b992ba..f75fcbc8c97 100644 --- a/steel-registry/build/entities.rs +++ b/steel-registry/build/entities.rs @@ -29,6 +29,7 @@ struct EntityTypeEntry { class_hierarchy: Vec, #[serde(default = "default_can_serialize")] can_serialize: bool, + only_op_can_set_nbt: bool, #[serde(default)] flags: Option, #[serde(default)] @@ -180,6 +181,7 @@ pub(crate) fn build() -> TokenStream { let allowed_in_peaceful = entity_type.allowed_in_peaceful; let can_spawn_far = entity_type.can_spawn_far_from_player; let can_serialize = entity_type.can_serialize; + let only_op_can_set_nbt = entity_type.only_op_can_set_nbt; let is_abstract_boat = entity_type .class_hierarchy .iter() @@ -261,6 +263,7 @@ pub(crate) fn build() -> TokenStream { allowed_in_peaceful: #allowed_in_peaceful, can_spawn_far_from_player: #can_spawn_far, can_serialize: #can_serialize, + only_op_can_set_nbt: #only_op_can_set_nbt, is_abstract_boat: #is_abstract_boat, is_abstract_minecart: #is_abstract_minecart, is_projectile: #is_projectile, diff --git a/steel-registry/build_assets/entities.json b/steel-registry/build_assets/entities.json index b2aa9fc8339..8a014242379 100644 --- a/steel-registry/build_assets/entities.json +++ b/steel-registry/build_assets/entities.json @@ -63,6 +63,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -297,6 +298,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -540,6 +542,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.allay.Allay", "class_hierarchy": [ @@ -833,6 +836,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.AreaEffectCloud", "class_hierarchy": [ @@ -1048,6 +1052,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.armadillo.Armadillo", "class_hierarchy": [ @@ -1366,6 +1371,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.ArmorStand", "class_hierarchy": [ @@ -1706,6 +1712,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.arrow.Arrow", "class_hierarchy": [ @@ -1937,6 +1944,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.axolotl.Axolotl", "class_hierarchy": [ @@ -2280,6 +2288,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestRaft", "class_hierarchy": [ @@ -2514,6 +2523,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Raft", "class_hierarchy": [ @@ -2753,6 +2763,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.ambient.Bat", "class_hierarchy": [ @@ -3061,6 +3072,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.bee.Bee", "class_hierarchy": [ @@ -3393,6 +3405,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -3627,6 +3640,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -3874,6 +3888,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Blaze", "class_hierarchy": [ @@ -4166,6 +4181,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.Display$BlockDisplay", "class_hierarchy": [ @@ -4507,6 +4523,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.skeleton.Bogged", "class_hierarchy": [ @@ -4820,6 +4837,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.breeze.Breeze", "class_hierarchy": [ @@ -5106,6 +5124,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.BreezeWindCharge", "class_hierarchy": [ @@ -5301,6 +5320,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.camel.Camel", "class_hierarchy": [ @@ -5674,6 +5694,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.camel.CamelHusk", "class_hierarchy": [ @@ -6047,6 +6068,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.feline.Cat", "class_hierarchy": [ @@ -6447,6 +6469,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.spider.CaveSpider", "class_hierarchy": [ @@ -6747,6 +6770,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -6981,6 +7005,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -7219,6 +7244,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartChest", "class_hierarchy": [ @@ -7464,6 +7490,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.chicken.Chicken", "class_hierarchy": [ @@ -7815,6 +7842,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.fish.Cod", "class_hierarchy": [ @@ -8131,6 +8159,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.golem.CopperGolem", "class_hierarchy": [ @@ -8434,6 +8463,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": true, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartCommandBlock", "class_hierarchy": [ @@ -8701,6 +8731,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.cow.Cow", "class_hierarchy": [ @@ -9048,6 +9079,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.creaking.Creaking", "class_hierarchy": [ @@ -9383,6 +9415,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Creeper", "class_hierarchy": [ @@ -9695,6 +9728,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -9929,6 +9963,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -10180,6 +10215,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.dolphin.Dolphin", "class_hierarchy": [ @@ -10535,6 +10571,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.Donkey", "class_hierarchy": [ @@ -10879,6 +10916,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.DragonFireball", "class_hierarchy": [ @@ -11066,6 +11104,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.zombie.Drowned", "class_hierarchy": [ @@ -11387,6 +11426,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg", "class_hierarchy": [ @@ -11595,6 +11635,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.ElderGuardian", "class_hierarchy": [ @@ -11916,6 +11957,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.EnderMan", "class_hierarchy": [ @@ -12243,6 +12285,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Endermite", "class_hierarchy": [ @@ -12572,6 +12615,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.boss.enderdragon.EnderDragon", "class_hierarchy": [ @@ -12863,6 +12907,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl", "class_hierarchy": [ @@ -13046,6 +13091,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.boss.enderdragon.EndCrystal", "class_hierarchy": [ @@ -13261,6 +13307,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.illager.Evoker", "class_hierarchy": [ @@ -13579,6 +13626,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.EvokerFangs", "class_hierarchy": [ @@ -13745,6 +13793,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownExperienceBottle", "class_hierarchy": [ @@ -13928,6 +13977,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.ExperienceOrb", "class_hierarchy": [ @@ -14096,6 +14146,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.EyeOfEnder", "class_hierarchy": [ @@ -14267,6 +14318,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": true, "synched_data": { "java_class": "net.minecraft.world.entity.item.FallingBlockEntity", "class_hierarchy": [ @@ -14451,6 +14503,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.LargeFireball", "class_hierarchy": [ @@ -14638,6 +14691,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.FireworkRocketEntity", "class_hierarchy": [ @@ -14856,6 +14910,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.fox.Fox", "class_hierarchy": [ @@ -15224,6 +15279,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.frog.Frog", "class_hierarchy": [ @@ -15557,6 +15613,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartFurnace", "class_hierarchy": [ @@ -15800,6 +15857,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Ghast", "class_hierarchy": [ @@ -16120,6 +16178,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.happyghast.HappyGhast", "class_hierarchy": [ @@ -16464,6 +16523,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Giant", "class_hierarchy": [ @@ -16750,6 +16810,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.GlowItemFrame", "class_hierarchy": [ @@ -16981,6 +17042,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.squid.GlowSquid", "class_hierarchy": [ @@ -17323,6 +17385,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.goat.Goat", "class_hierarchy": [ @@ -17675,6 +17738,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Guardian", "class_hierarchy": [ @@ -17996,6 +18060,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.hoglin.Hoglin", "class_hierarchy": [ @@ -18322,6 +18387,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartHopper", "class_hierarchy": [ @@ -18571,6 +18637,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.Horse", "class_hierarchy": [ @@ -18928,6 +18995,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.zombie.Husk", "class_hierarchy": [ @@ -19274,6 +19342,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.illager.Illusioner", "class_hierarchy": [ @@ -19592,6 +19661,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.Interaction", "class_hierarchy": [ @@ -19797,6 +19867,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.golem.IronGolem", "class_hierarchy": [ @@ -20085,6 +20156,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.item.ItemEntity", "class_hierarchy": [ @@ -20257,6 +20329,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.Display$ItemDisplay", "class_hierarchy": [ @@ -20589,6 +20662,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.ItemFrame", "class_hierarchy": [ @@ -20795,6 +20869,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -21029,6 +21104,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -21259,6 +21335,7 @@ "allowed_in_peaceful": true, "can_serialize": false, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.LeashFenceKnotEntity", "class_hierarchy": [ @@ -21417,6 +21494,7 @@ "allowed_in_peaceful": true, "can_serialize": false, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.LightningBolt", "class_hierarchy": [ @@ -21604,6 +21682,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.Llama", "class_hierarchy": [ @@ -21966,6 +22045,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.LlamaSpit", "class_hierarchy": [ @@ -22149,6 +22229,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.cubemob.MagmaCube", "class_hierarchy": [ @@ -22471,6 +22552,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -22705,6 +22787,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -22939,6 +23022,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.Mannequin", "class_hierarchy": [ @@ -23244,6 +23328,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.Marker", "class_hierarchy": [ @@ -23406,6 +23491,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.Minecart", "class_hierarchy": [ @@ -23651,6 +23737,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.cow.MushroomCow", "class_hierarchy": [ @@ -24002,6 +24089,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.Mule", "class_hierarchy": [ @@ -24371,6 +24459,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.nautilus.Nautilus", "class_hierarchy": [ @@ -24726,6 +24815,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -24960,6 +25050,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -25211,6 +25302,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.feline.Ocelot", "class_hierarchy": [ @@ -25526,6 +25618,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.OminousItemSpawner", "class_hierarchy": [ @@ -25702,6 +25795,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.decoration.painting.Painting", "class_hierarchy": [ @@ -25900,6 +25994,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -26134,6 +26229,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -26385,6 +26481,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.panda.Panda", "class_hierarchy": [ @@ -26765,6 +26862,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.skeleton.Parched", "class_hierarchy": [ @@ -27076,6 +27174,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.parrot.Parrot", "class_hierarchy": [ @@ -27437,6 +27536,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Phantom", "class_hierarchy": [ @@ -27742,6 +27842,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.pig.Pig", "class_hierarchy": [ @@ -28097,6 +28198,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.piglin.Piglin", "class_hierarchy": [ @@ -28444,6 +28546,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.piglin.PiglinBrute", "class_hierarchy": [ @@ -28769,6 +28872,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.illager.Pillager", "class_hierarchy": [ @@ -29108,6 +29212,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.polarbear.PolarBear", "class_hierarchy": [ @@ -29439,6 +29544,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownSplashPotion", "class_hierarchy": [ @@ -29642,6 +29748,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownLingeringPotion", "class_hierarchy": [ @@ -29854,6 +29961,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.fish.Pufferfish", "class_hierarchy": [ @@ -30184,6 +30292,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.rabbit.Rabbit", "class_hierarchy": [ @@ -30528,6 +30637,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Ravager", "class_hierarchy": [ @@ -30853,6 +30963,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.fish.Salmon", "class_hierarchy": [ @@ -31187,6 +31298,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.sheep.Sheep", "class_hierarchy": [ @@ -31522,6 +31634,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Shulker", "class_hierarchy": [ @@ -31829,6 +31942,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.ShulkerBullet", "class_hierarchy": [ @@ -32008,6 +32122,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Silverfish", "class_hierarchy": [ @@ -32307,6 +32422,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.skeleton.Skeleton", "class_hierarchy": [ @@ -32628,6 +32744,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.SkeletonHorse", "class_hierarchy": [ @@ -32971,6 +33088,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.cubemob.Slime", "class_hierarchy": [ @@ -33297,6 +33415,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.SmallFireball", "class_hierarchy": [ @@ -33505,6 +33624,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.sniffer.Sniffer", "class_hierarchy": [ @@ -33839,6 +33959,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball", "class_hierarchy": [ @@ -34043,6 +34164,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.golem.SnowGolem", "class_hierarchy": [ @@ -34338,6 +34460,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": true, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartSpawner", "class_hierarchy": [ @@ -34562,6 +34685,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.arrow.SpectralArrow", "class_hierarchy": [ @@ -34775,6 +34899,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.spider.Spider", "class_hierarchy": [ @@ -35071,6 +35196,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.Boat", "class_hierarchy": [ @@ -35305,6 +35431,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.boat.ChestBoat", "class_hierarchy": [ @@ -35556,6 +35683,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.squid.Squid", "class_hierarchy": [ @@ -35880,6 +36008,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.skeleton.Stray", "class_hierarchy": [ @@ -36183,6 +36312,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Strider", "class_hierarchy": [ @@ -36530,6 +36660,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.cubemob.SulfurCube", "class_hierarchy": [ @@ -36891,6 +37022,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.frog.Tadpole", "class_hierarchy": [ @@ -37201,6 +37333,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.Display$TextDisplay", "class_hierarchy": [ @@ -37549,6 +37682,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.item.PrimedTnt", "class_hierarchy": [ @@ -37733,6 +37867,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.vehicle.minecart.MinecartTNT", "class_hierarchy": [ @@ -37986,6 +38121,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.TraderLlama", "class_hierarchy": [ @@ -38356,6 +38492,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.arrow.ThrownTrident", "class_hierarchy": [ @@ -38599,6 +38736,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.fish.TropicalFish", "class_hierarchy": [ @@ -38933,6 +39071,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.turtle.Turtle", "class_hierarchy": [ @@ -39276,6 +39415,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Vex", "class_hierarchy": [ @@ -39589,6 +39729,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.npc.villager.Villager", "class_hierarchy": [ @@ -39961,6 +40102,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.illager.Vindicator", "class_hierarchy": [ @@ -40286,6 +40428,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.npc.wanderingtrader.WanderingTrader", "class_hierarchy": [ @@ -40620,6 +40763,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.warden.Warden", "class_hierarchy": [ @@ -40920,6 +41064,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge", "class_hierarchy": [ @@ -41115,6 +41260,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Witch", "class_hierarchy": [ @@ -41446,6 +41592,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.boss.wither.WitherBoss", "class_hierarchy": [ @@ -41784,6 +41931,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.skeleton.WitherSkeleton", "class_hierarchy": [ @@ -42070,6 +42218,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.hurtingprojectile.WitherSkull", "class_hierarchy": [ @@ -42275,6 +42424,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.wolf.Wolf", "class_hierarchy": [ @@ -42671,6 +42821,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.Zoglin", "class_hierarchy": [ @@ -42980,6 +43131,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.zombie.Zombie", "class_hierarchy": [ @@ -43314,6 +43466,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.equine.ZombieHorse", "class_hierarchy": [ @@ -43665,6 +43818,7 @@ "allowed_in_peaceful": true, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.animal.nautilus.ZombieNautilus", "class_hierarchy": [ @@ -44051,6 +44205,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.zombie.ZombieVillager", "class_hierarchy": [ @@ -44420,6 +44575,7 @@ "allowed_in_peaceful": false, "can_serialize": true, "can_spawn_far_from_player": false, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.monster.zombie.ZombifiedPiglin", "class_hierarchy": [ @@ -44737,6 +44893,7 @@ "allowed_in_peaceful": true, "can_serialize": false, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.player.Player", "class_hierarchy": [ @@ -45051,6 +45208,7 @@ "allowed_in_peaceful": true, "can_serialize": false, "can_spawn_far_from_player": true, + "only_op_can_set_nbt": false, "synched_data": { "java_class": "net.minecraft.world.entity.projectile.FishingHook", "class_hierarchy": [ diff --git a/steel-registry/src/entity_type.rs b/steel-registry/src/entity_type.rs index bc2b143741a..5757b17e476 100644 --- a/steel-registry/src/entity_type.rs +++ b/steel-registry/src/entity_type.rs @@ -295,6 +295,9 @@ pub struct EntityType { /// Whether this entity type can be serialized to disk. /// Set to false for transient entities (lightning, fishing hooks, players). pub can_serialize: bool, + /// Whether only server operators may set this entity's data via a spawn + /// item's `ENTITY_DATA` component. Mirrors vanilla `EntityType.onlyOpCanSetNbt`. + pub only_op_can_set_nbt: bool, /// Whether vanilla class hierarchy makes this entity an `AbstractBoat`. pub is_abstract_boat: bool, /// Whether vanilla class hierarchy makes this entity an `AbstractMinecart`.