Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions steel-core/src/behavior/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ use crate::world::{
};
use steel_registry::vanilla_fluids;

/// Maximum light emission a block may emit while still allowing mobs to spawn
/// on it, mirroring vanilla `BlockBehaviour`'s default `isValidSpawn` predicate
/// (`state.getLightEmission() < 14`).
Comment on lines +50 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sure it can be sorter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean shorter?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes x)

pub(crate) const MAX_MOB_SPAWN_LIGHT_EMISSION: u8 = 14;

/// Vanilla `BlockBehaviour.canBeReplaced(BlockState, BlockPlaceContext)`.
pub(crate) fn default_can_be_replaced(
state: BlockStateId,
Expand Down Expand Up @@ -221,6 +226,25 @@ pub trait BlockBehavior: Send + Sync {
true
}

/// Returns whether this block accepts a mob spawning on or inside it.
///
/// Vanilla parity: `BlockState.isValidSpawn(BlockGetter, BlockPos, EntityType)`, whose
/// default requires a full UP support face and light emission below
/// [`MAX_MOB_SPAWN_LIGHT_EMISSION`]. Blocks override it for special spawn surfaces
/// (soul sand accepts everything, magma only fire-immune mobs, ...). Consulted by
/// `Mob.checkMobSpawnRules`.
Comment on lines +231 to +235

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

look a bit long

fn is_valid_spawn(
&self,
state: BlockStateId,
world: &dyn LevelReader,
pos: BlockPos,
_entity_type: EntityTypeRef,
) -> bool {
// Vanilla evaluation order: full UP support first, then the light-emission gate.
world.is_face_sturdy_for(state, pos, Direction::Up, SupportType::Full)
&& state.get_light_emission() < MAX_MOB_SPAWN_LIGHT_EMISSION
}

/// Returns whether this block can be occupied by a forced respawn position
fn is_possible_to_respawn_in_this(&self, state: BlockStateId) -> bool {
!state.is_solid() && !state.get_block().config.liquid
Expand Down
37 changes: 37 additions & 0 deletions steel-core/src/behavior/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,47 @@ use steel_registry::blocks::properties::{BlockStateProperties, SlabType};
use steel_registry::init_vanilla_registry;
use steel_registry::sound_events;
use steel_registry::vanilla_blocks;
use steel_registry::vanilla_entities;
use steel_registry::vanilla_items;

use crate::behavior::init_behaviors;

#[test]
fn default_spawn_rule_requires_full_up_support_and_low_light_emission() {
init_vanilla_registry();
init_behaviors();
let level = TestLevel::default();
let pos = BlockPos::new(3, 64, 5);
let entity_type = &vanilla_entities::ZOMBIE;

// Stone is a full support surface with no light emission: mobs may spawn.
let stone = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::STONE);
assert!(stone.is_valid_spawn(
vanilla_blocks::STONE.default_state(),
&level,
pos,
entity_type
));

// Glowstone is full-support but emits light at or above the gate: blocked.
let glowstone = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::GLOWSTONE);
assert!(!glowstone.is_valid_spawn(
vanilla_blocks::GLOWSTONE.default_state(),
&level,
pos,
entity_type
));

// A torch has no full UP support face: blocked even though it is dark.
let torch = BLOCK_BEHAVIORS.get_behavior(&vanilla_blocks::TORCH);
assert!(!torch.is_valid_spawn(
vanilla_blocks::TORCH.default_state(),
&level,
pos,
entity_type
));
}

#[test]
fn clone_item_stack_uses_registered_block_item_association() {
init_vanilla_registry();
Expand Down
15 changes: 14 additions & 1 deletion steel-core/src/behavior/blocks/building/magma_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ use std::sync::Arc;

use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_registry::entity_type::EntityTypeRef;
use steel_registry::vanilla_damage_types;
use steel_utils::{BlockPos, BlockStateId};

use crate::{
behavior::{BlockBehavior, BlockPlaceContext},
entity::{Entity, damage::DamageSource},
world::World,
world::{LevelReader, World},
};

/// Behavior for magma blocks.
Expand Down Expand Up @@ -42,6 +43,18 @@ impl BlockBehavior for MagmaBlock {
Some(self.block.default_state())
}

/// Vanilla magma registers `isValidSpawn(entityType -> entityType.fireImmune())`:
/// only fire-immune mobs (magma cubes, blazes) spawn on it.
fn is_valid_spawn(
&self,
_state: BlockStateId,
_world: &dyn LevelReader,
_pos: BlockPos,
entity_type: EntityTypeRef,
) -> bool {
entity_type.fire_immune
}

fn step_on(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
if let Some(damage) =
Self::step_damage_amount(entity.is_stepping_carefully(), entity.is_living_entity())
Expand Down
16 changes: 15 additions & 1 deletion steel-core/src/behavior/blocks/building/soul_sand_block.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use steel_macros::block_behavior;
use steel_registry::blocks::BlockRef;
use steel_utils::BlockStateId;
use steel_registry::entity_type::EntityTypeRef;
use steel_utils::{BlockPos, BlockStateId};

use crate::{
behavior::{BlockBehavior, BlockPlaceContext},
entity::ai::path::PathComputationType,
world::LevelReader,
};

/// Soul sand. Mobs will not pathfind through this block.
Expand All @@ -26,6 +28,18 @@ impl BlockBehavior for SoulSandBlock {
Some(self.block.default_state())
}

/// Vanilla soul sand uses `Blocks::always` for `isValidSpawn`: any mob may
/// spawn on it (wither skeletons, nether mobs).
fn is_valid_spawn(
&self,
_state: BlockStateId,
_world: &dyn LevelReader,
_pos: BlockPos,
_entity_type: EntityTypeRef,
) -> bool {
true
}

fn is_pathfindable(
&self,
_state: BlockStateId,
Expand Down
4 changes: 3 additions & 1 deletion steel-core/src/entity/ai/goal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ pub(crate) use melee_attack::MeleeAttackGoal;
pub(crate) use nearest_attackable_target::NearestAttackableTargetGoal;
pub(crate) use panic_goal::PanicGoal;
pub(crate) use random_look_around::RandomLookAroundGoal;
pub(crate) use selector::{GoalControl, GoalSelector};
#[cfg(test)]
pub(crate) use selector::GoalControls;
pub(crate) use selector::{Goal, GoalControl, GoalSelector};
pub(crate) use tempt_goal::TemptGoal;
pub(crate) use water_avoiding_random_stroll::WaterAvoidingRandomStrollGoal;

Expand Down
66 changes: 66 additions & 0 deletions steel-core/src/entity/ai/goal/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ impl WrappedGoal {
self.running
}

fn goal(&self) -> &dyn Goal {
self.goal.as_ref()
}

fn controls(&self) -> GoalControls {
self.goal.controls()
}
Expand Down Expand Up @@ -188,6 +192,24 @@ impl GoalSelector {
.push(WrappedGoal::new(priority, Box::new(goal)));
}

/// Removes every goal matching `predicate`, stopping running matches first.
///
/// Mirrors vanilla `GoalSelector.removeAllGoals`. The mob is required only
/// because Steel's `Goal.stop` receives the mob for its stop hook.
pub fn remove_all_goals(
&mut self,
mob: &dyn PathfinderMob,
mut predicate: impl FnMut(&dyn Goal) -> bool,
) {
for wrapped in &mut self.available_goals {
if predicate(wrapped.goal()) && wrapped.is_running() {
wrapped.stop(mob);
}
}
self.available_goals
.retain(|wrapped| !predicate(wrapped.goal()));
}

pub fn tick(&mut self, mob: &dyn PathfinderMob) {
for index in 0..self.available_goals.len() {
let should_stop = {
Expand Down Expand Up @@ -612,4 +634,48 @@ mod tests {
);
assert!(mob.is_panicking());
}

#[test]
fn remove_all_goals_stops_matching_running_goals_and_keeps_others() {
let mob = TestPathfinderMob::new();
let mut selector = GoalSelector::new();
selector.add_goal(3, StaticGoal::new(GoalControls::MOVE));
selector.add_goal(7, StaticGoal::new(GoalControls::LOOK));
selector.tick(&mob);
assert_eq!(selector.running_goal_count(), 2);

selector.remove_all_goals(&mob, |goal| goal.controls() == GoalControls::MOVE);

assert_eq!(selector.available_goal_count(), 1);
assert!(
selector.is_priority_running(7),
"the non-matching goal must keep running through removal"
);
assert!(
!selector.is_priority_running(3),
"the removed goal must be gone entirely"
);

selector.tick(&mob);
assert_eq!(selector.running_goal_count(), 1);
assert!(selector.is_priority_running(7));
}

#[test]
fn pathfinder_mob_remove_free_will_clears_all_goals() {
let mob = TestPathfinderMob::new();
{
let mut selector = mob.mob_base().goal_selector().lock();
selector.add_goal(1, StaticGoal::new(GoalControls::MOVE));
selector.add_goal(2, StaticGoal::new(GoalControls::LOOK));
selector.tick(&mob);
assert_eq!(selector.running_goal_count(), 2);
}

mob.remove_free_will();

let selector = mob.mob_base().goal_selector().lock();
assert_eq!(selector.available_goal_count(), 0);
assert_eq!(selector.running_goal_count(), 0);
}
}
Loading
Loading