Compare commits

...
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 43b029b5d9 Natural spawning: test a position's placement before its spawn list
Every position a spawn group tries away from where its entry was picked
asked mobsAt again (structures reaching it, the biome, a string-keyed
list) before isSpawnPositionOk, which turns most positions away with a
few block reads. Both only read the level, so the placement now goes
first. isSpawnPositionOk also looked its placement up by an id built with
format! after a linear search for the species' definition: MobData keeps
a table by species.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 16:55:12 +01:00
5 changed files with 41 additions and 7 deletions

No files matched your search

@@ -1192,6 +1192,7 @@ mod preview_tests {
entity_tags: Default::default(),
villager_hats: Default::default(),
pois: Default::default(),
species_placements: Default::default(),
};
let draw = |body: bool| {
let mut out = Vec::new();
+28 -2
View File
@@ -33,7 +33,7 @@ pub struct StateData {
/// Collision shape top (0 when the shape is empty).
pub top: f32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placement {
OnGround,
InWater,
@@ -55,6 +55,9 @@ pub struct MobData {
/// The villager POI type of every block state (`PoiTypes.forState`),
/// worked out the first time a villager looks for one.
pub pois: std::sync::OnceLock<Vec<Option<super::villager_brain::Poi>>>,
/// [`MobData::placement`] of each species' entity type (by `Species as
/// usize`), worked out the first time a spawn asks.
pub species_placements: std::sync::OnceLock<Vec<Placement>>,
}
impl MobData {
pub fn load(root: &Path, facts: &BlockFacts) -> Self {
@@ -145,6 +148,7 @@ impl MobData {
biome_tags: HashMap::new(),
entity_tags: HashMap::new(),
pois: Default::default(),
species_placements: Default::default(),
}
}
/// Without the export, approximate path types from the gameplay facts.
@@ -201,6 +205,7 @@ impl MobData {
biome_tags: HashMap::new(),
entity_tags: HashMap::new(),
pois: Default::default(),
species_placements: Default::default(),
}
}
pub fn state(&self, id: horizon_world::registry::BlockStateId) -> StateData {
@@ -222,7 +227,28 @@ impl MobData {
pub fn placement(&self, entity: &str) -> Placement {
self.placements
.get(entity)
.cloned()
.copied()
.unwrap_or(Placement::OnGround)
}
/// [`MobData::placement`] of the species' entity type, without building
/// its id: natural spawning asks it for every position it tries.
pub fn species_placement(&self, s: super::Species) -> Placement {
let table = self.species_placements.get_or_init(|| {
let defs = super::species::ALL;
let len = defs
.iter()
.map(|d| d.species as usize + 1)
.max()
.unwrap_or(0);
let mut table = vec![Placement::OnGround; len];
// `species::def` takes a species' first definition: so does this.
for d in defs.iter().rev() {
table[d.species as usize] = self.placement(&format!("minecraft:{}", d.name));
}
table
});
table.get(s as usize).copied().unwrap_or_else(|| {
self.placement(&format!("minecraft:{}", super::species::def(s).name))
})
}
}
+4
View File
@@ -1887,6 +1887,7 @@ mod tests {
entity_tags: Default::default(),
villager_hats: Default::default(),
pois: Default::default(),
species_placements: Default::default(),
};
let atlas = |name: &str| {
Some(if name.contains("_inner") {
@@ -1975,6 +1976,7 @@ mod tests {
entity_tags: Default::default(),
villager_hats: Default::default(),
pois: Default::default(),
species_placements: Default::default(),
};
let atlas = |name: &str| {
Some(if name.contains("copper_golem_eyes") {
@@ -2027,6 +2029,7 @@ mod tests {
entity_tags: Default::default(),
villager_hats: Default::default(),
pois: Default::default(),
species_placements: Default::default(),
};
for left in [false, true] {
let t = held_item_transform(&mob, 0.5, glam::DVec3::ZERO, &data, left).unwrap();
@@ -2112,6 +2115,7 @@ mod tests {
entity_tags: Default::default(),
villager_hats: Default::default(),
pois: Default::default(),
species_placements: Default::default(),
};
let atlas = |_: &str| Some([0.0, 0.0, 0.25, 0.25]);
let part = &kind.model;
@@ -244,8 +244,7 @@ pub fn is_valid_empty_spawn<L: SpawnLevel>(s: Species, l: &L, x: i32, y: i32, z:
}
/// `SpawnPlacements.isSpawnPositionOk`.
pub fn is_spawn_position_ok<L: SpawnLevel>(s: Species, l: &L, x: i32, y: i32, z: i32) -> bool {
let id = format!("minecraft:{}", super::species::def(s).name);
match l.mob_data().placement(&id) {
match l.mob_data().species_placement(s) {
data::Placement::OnGround => {
is_valid_spawn(s, l, x, y - 1, z)
&& is_valid_empty_spawn(s, l, x, y, z)
@@ -1209,6 +1209,13 @@ fn valid_position_for_type<L: NaturalLevel, R: RandomSource>(
return false;
}
let [x, y, z] = pos;
// `canSpawnMobAt` comes first in vanilla, but it and `isSpawnPositionOk`
// only read the level, so their order cannot change the answer: the few
// block reads of the placement turn most positions away before the spawn
// list (structures, biome) is looked up again.
if !spawn_rules::is_spawn_position_ok(species, l, x, y, z) {
return false;
}
if !listed
&& !mobs_at(l, category, x, y, z)
.iter()
@@ -1216,9 +1223,6 @@ fn valid_position_for_type<L: NaturalLevel, R: RandomSource>(
{
return false;
}
if !spawn_rules::is_spawn_position_ok(species, l, x, y, z) {
return false;
}
if !spawn_rules::check_spawn_rules(species, l, Reason::Natural, random, x, y, z) {
return false;
}