Merge branch 'heads/pt3/2586' into union/pt3-2

This commit is contained in:
Ludovic Marie committed 2026-10-29 10:30:23 +01:00
commit 285ecb6e89
15 files changed
+261 -87

No files matched your search

@@ -142,8 +142,8 @@ const GENES: [&str; 7] = [
"aggressive",
];
const MEMORIES: [&str; 7] = [
"long_jump_cooldown",
"ram_cooldown",
"long_jump_cooling_down",
"ram_cooldown_ticks",
"attack_target_cooldown",
"hunted_recently",
"dig_cooldown",
@@ -795,7 +795,7 @@ pub fn main() -> horizon_client::Result<()> {
chunk,
&mut gen_random,
&mut rules,
&mut spawn,
&mut |l, r, _| spawn(l, r).is_some(),
&mut |e| match e {
GenEvent::Column(x, z) => log.push(format!("col {x} {z}")),
GenEvent::Spawn(t, p, yaw) => spawns.push(format!(
+17 -8
View File
@@ -744,22 +744,31 @@ pub fn mob_to_nbt(m: &Mob, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
if let Some(a) = m.extra.attributes.as_deref() {
put_attributes(map, m, a);
}
// Zombie and Piglin.setBaby, which their load runs even for an adult, make
// the speed attribute that AttributeMap.pack then saves.
if crate::mobs::doors::is_zombie(species) || species == crate::mobs::Species::Piglin {
let speed = crate::mobs::finalize::Attr::MovementSpeed;
let base = crate::mobs::finalize::base_value(m, speed);
// The attribute their load makes, which AttributeMap.pack then saves:
// Zombie and Piglin.setBaby (even for an adult) the speed, Rabbit.setVariant
// the attack damage.
use crate::mobs::finalize::Attr;
let made = if crate::mobs::doors::is_zombie(species) || species == crate::mobs::Species::Piglin
{
Some(Attr::MovementSpeed)
} else if species == crate::mobs::Species::Rabbit {
Some(Attr::AttackDamage)
} else {
None
};
if let Some(attr) = made {
let base = crate::mobs::finalize::base_value(m, attr);
let mut list = match map.get(&"attributes".into()) {
Some(Tag::List(l)) => l.clone(),
_ => Vec::new(),
};
let speed_id = format!("minecraft:{}", speed.name());
let attr_id = format!("minecraft:{}", attr.name());
if !list
.iter()
.any(|t| text(t.get("id")).as_deref() == Some(speed_id.as_str()))
.any(|t| text(t.get("id")).as_deref() == Some(attr_id.as_str()))
{
list.push(Tag::compound([
("id", string(&speed_id)),
("id", string(&attr_id)),
("base", Tag::Double(base.to_bits())),
]));
map.insert("attributes".into(), Tag::List(list));
+1
View File
@@ -12715,6 +12715,7 @@ impl Game<'_> {
let (uploads, unloaded) =
horizon_client::spans::time("streaming", || self.streaming.update(&mut self.world));
self.streaming.extra.truncate(areas);
self.spawn_fresh_chunks();
if let Some(lod) = &mut self.lod {
self.lod_observe
.extend(uploads.iter().map(|(key, _)| (key.0, key.2)));
+7 -3
View File
@@ -514,9 +514,9 @@ impl Game<'_> {
};
spawning::LocalDifficulty::new(base, self.world.day_time, inhabited, moon)
}
/// Advance every mob one tick, then apply their events.
pub(super) fn tick_creatures(&mut self) -> Result<()> {
self.attack_ticks = self.attack_ticks.saturating_add(1);
/// Generation's animals for the chunks generated since, once lit: chunk
/// generation places them whether the level ticks or not.
pub(super) fn spawn_fresh_chunks(&mut self) {
let fresh: Vec<(i32, i32)> = self
.world
.chunks
@@ -530,6 +530,10 @@ impl Game<'_> {
}
self.spawn_generation_creatures(key.0, key.1);
}
}
/// Advance every mob one tick, then apply their events.
pub(super) fn tick_creatures(&mut self) -> Result<()> {
self.attack_ticks = self.attack_ticks.saturating_add(1);
self.tick_leads();
self.spawn_natural();
// ServerChunkCache.tickChunks: the custom spawners tick only with spawn_mobs.
@@ -2,6 +2,7 @@
//! it reads, the spawning chunks and caps of `ServerChunkCache.tickChunks`,
//! and the mobs it adds (finalized with the game's `finalizeSpawn`).
use super::{Dim, Game};
use horizon_client::vanillagen::VanillaTerrain;
use horizon_client::{
biome::{BiomeInfo, SpawnEntry},
blocks::BlockFacts,
@@ -19,9 +20,12 @@ use horizon_client::{
};
use horizon_core::{
geometry::{Aabb, Vec3},
random::{LegacyRandom, RandomSource, WorldgenRandom},
random::{LegacyRandom, RandomSource, WorldgenRandom, XoroshiroRandom},
};
use horizon_world::{
nbt::{Compound, Tag},
registry::BlockStateId,
};
use horizon_world::registry::BlockStateId;
use std::collections::HashMap;
/// Reused buffers for one pass over the spawning chunks: the candidate chunks
@@ -157,6 +161,16 @@ impl GameLevel<'_, '_> {
let chunk = self.game.world.chunks.get(&(x >> 4, z >> 4))?;
Some(self.game.res.biomes().get(chunk.biome(x, y, z)))
}
/// The block whose noise cell `BiomeManager.getBiome` reads.
fn zoomed(&self, x: i32, y: i32, z: i32) -> [i32; 3] {
let zoom = self.game.streaming.biome_zoom;
let [qx, qy, qz] = zoom.quart_at_block(horizon_core::position::BlockPos::new(x, y, z));
[qx << 2, qy << 2, qz << 2]
}
fn zoomed_biome(&self, x: i32, y: i32, z: i32) -> Option<&BiomeInfo> {
let [x, y, z] = self.zoomed(x, y, z);
self.biome(x, y, z)
}
}
impl SpawnLevel for GameLevel<'_, '_> {
fn state(&self, x: i32, y: i32, z: i32) -> BlockStateId {
@@ -171,7 +185,9 @@ impl SpawnLevel for GameLevel<'_, '_> {
fn block_tag(&self, tag: &str, block: &str) -> bool {
self.w.block_tag(tag, block)
}
// Level.getBiome: the zoomed biome.
fn biome_tag(&self, tag: &str, x: i32, y: i32, z: i32) -> bool {
let [x, y, z] = self.zoomed(x, y, z);
self.w.biome_tag(tag, x, y, z)
}
fn sky_light(&self, x: i32, y: i32, z: i32) -> i32 {
@@ -237,11 +253,13 @@ impl NaturalLevel for GameLevel<'_, '_> {
Category::from_name(&t.category),
))
}
// The `natural_mob_spawns` attribute reads the zoomed biome
// (fullResolutionBiomes), the spawn probability the noise cell.
fn biome_spawns(&self, x: i32, y: i32, z: i32) -> Option<&HashMap<String, Vec<SpawnEntry>>> {
self.biome(x, y, z).map(|b| &b.spawns)
self.zoomed_biome(x, y, z).map(|b| &b.spawns)
}
fn spawn_cost(&self, entity: &str, x: i32, y: i32, z: i32) -> Option<SpawnCost> {
self.biome(x, y, z)?.spawn_costs.get(entity).copied()
self.zoomed_biome(x, y, z)?.spawn_costs.get(entity).copied()
}
fn creature_probability(&self, x: i32, y: i32, z: i32) -> f32 {
self.biome(x, y, z).map_or(0.1, |b| b.creature_probability)
@@ -573,37 +591,68 @@ impl<'a> Game<'a> {
}
/// `NoiseBasedChunkGenerator.spawnOriginalMobs`: creature packs for a
/// freshly generated chunk, from the biome at its corner column's top,
/// with the chunk's decoration-seeded random (mobs are not generated in
/// with the chunk's decoration-seeded random, finalized with the
/// region's random as generation adds them (mobs are not generated in
/// the End).
pub(super) fn spawn_generation_creatures(&mut self, cx: i32, cz: i32) {
if !self.world.chunks.contains_key(&(cx, cz)) || self.dim == Dim::End {
return;
}
let Some(terrain) = self.end.locator() else {
return;
};
let mut region = terrain.region_random(cx, cz);
let any = region.as_any();
let added = if let Some(r) = any.downcast_mut::<LegacyRandom>() {
self.generation_creatures(cx, cz, &terrain, r)
} else if let Some(r) = any.downcast_mut::<XoroshiroRandom>() {
self.generation_creatures(cx, cz, &terrain, r)
} else {
return;
};
for nbt in added {
self.load_mob(Tag::Compound(nbt), None);
}
}
fn generation_creatures<Q: RandomSource + 'static>(
&self,
cx: i32,
cz: i32,
terrain: &VanillaTerrain,
region: &mut Q,
) -> Vec<Compound> {
let mut random = WorldgenRandom::new(LegacyRandom::new(0));
random.set_decoration_seed(self.level.seed, cx * 16, cz * 16);
// The region's own random: no creature's spawn predicate draws from it.
let mut rules = LegacyRandom::new(self.level.seed ^ (i64::from(cx) << 32 | i64::from(cz)));
let mut pending = Vec::new();
{
let level = self.natural_level();
let source = [cx * 16, level.max_y(), cz * 16];
let mut spawn = |l: &GameLevel, r: &SpawnRequest| {
let spawned = l.game.finalize_spawned(r, &mut pending);
if let Some(s) = &spawned {
l.pending.borrow_mut().push(s.bounds);
let mut added = Vec::new();
let mut group = None;
let level = self.natural_level();
let source = [cx * 16, level.max_y(), cz * 16];
let mut spawn = |l: &GameLevel, r: &SpawnRequest, region: &mut Q| {
// Each pack starts without group data.
if r.group_index == 0 {
group = None;
}
let [x, y, z] = r.position;
let (bx, by, bz) = (x.floor() as i32, y.floor() as i32, z.floor() as i32);
let biome = l.zoomed_biome(bx, by, bz).map_or("", |b| b.name.as_str());
let at = Vec3::new(x, y, z);
match terrain.spawn_generated(r.entity, at, r.yaw, biome, region, &mut group) {
Some(nbt) => {
added.push(nbt);
true
}
spawned
};
spawn_selection::spawn_mobs_for_chunk_generation(
&level,
source,
(cx, cz),
&mut random,
&mut rules,
&mut spawn,
&mut |_| {},
);
}
self.add_spawned(pending, true);
None => false,
}
};
spawn_selection::spawn_mobs_for_chunk_generation(
&level,
source,
(cx, cz),
&mut random,
region,
&mut spawn,
&mut |_| {},
);
added
}
}
+3 -3
View File
@@ -1619,7 +1619,7 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
S::Frog => {
self.variant(m, |r| &r.frog);
let jump = self.uniform(100, 140);
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
set_memory(m, "long_jump_cooling_down", i64::from(jump), i64::MAX);
self.ageable(m, g)
}
S::ZombieNautilus => {
@@ -1734,9 +1734,9 @@ impl<L: FinalizeLevel + ?Sized, R: RandomSource> Ctx<'_, L, R> {
}
S::Goat => {
let jump = self.uniform(600, 1200);
set_memory(m, "long_jump_cooldown", i64::from(jump), i64::MAX);
set_memory(m, "long_jump_cooling_down", i64::from(jump), i64::MAX);
let ram = self.uniform(600, 6000);
set_memory(m, "ram_cooldown", i64::from(ram), i64::MAX);
set_memory(m, "ram_cooldown_ticks", i64::from(ram), i64::MAX);
m.extra.screaming = self.random.next_double() < 0.02;
age_boundary_reached(m);
if !m.is_baby() && self.float() < 0.1 {
@@ -539,18 +539,21 @@ pub fn spawn_aabb(s: Species, x: f64, y: f64, z: f64, width: f32, height: f32) -
Vec3::new(x + half, y + f64::from(scale * height), z + half),
)
}
/// `checkSpawnObstruction` of a freshly created mob with the given box.
/// `checkSpawnObstruction` of a freshly created mob with the given box, with
/// the mobs already `spawned` in the way; None in a generation region, which
/// has no entities (`WorldGenRegion.getEntities`).
pub(crate) fn spawn_obstruction<L: NaturalLevel>(
s: Species,
l: &L,
bounds: Aabb,
spawned: &[Aabb],
spawned: Option<&[Aabb]>,
x: i32,
y: i32,
z: i32,
) -> bool {
let unobstructed =
|l: &L| l.unobstructed(bounds) && !spawned.iter().any(|b| b.intersects(bounds));
let unobstructed = |l: &L| {
spawned.is_none_or(|s| l.unobstructed(bounds) && !s.iter().any(|b| b.intersects(bounds)))
};
match obstruction(s) {
Obstruction::NoLiquidUnobstructed => !contains_any_liquid(l, bounds) && unobstructed(l),
Obstruction::Unobstructed => unobstructed(l),
@@ -1084,7 +1087,7 @@ pub fn spawn_category_for_position<L: NaturalLevel, R: RandomSource>(
let far = distance > despawn && removed_when_far(species);
if far
|| !mob_spawn_rules(species, l, x, y, z)
|| !spawn_obstruction(species, l, bounds, &spawned, x, y, z)
|| !spawn_obstruction(species, l, bounds, Some(&spawned), x, y, z)
{
continue;
}
@@ -1237,8 +1240,8 @@ pub enum GenEvent {
Spawn(String, [f64; 3], f32),
}
/// `NaturalSpawner.spawnMobsForChunkGeneration`: creature packs placed on
/// the chunk's surface. `rules` is the level's own random, which the spawn
/// predicates draw from.
/// the chunk's surface. `rules` is the region's random, which the spawn
/// predicates and `spawn` (finalizeSpawn) draw from.
#[allow(clippy::too_many_arguments)]
pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: RandomSource>(
l: &L,
@@ -1246,7 +1249,7 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
chunk: (i32, i32),
random: &mut R,
rules: &mut Q,
spawn: &mut dyn FnMut(&L, &SpawnRequest) -> Option<Spawned>,
spawn: &mut dyn FnMut(&L, &SpawnRequest, &mut Q) -> bool,
trace: &mut dyn FnMut(GenEvent),
) {
if !l.spawn_mobs() {
@@ -1269,7 +1272,6 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
};
let count = sample_count(&entry, random);
let mut group_index = 0u32;
let mut spawned: Vec<Aabb> = Vec::new();
let mut x = xo + random.next_int_bound(16).unwrap_or(0);
let mut z = zo + random.next_int_bound(16).unwrap_or(0);
let (start_x, start_z) = (x, z);
@@ -1311,7 +1313,7 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
let yaw = random.next_float() * 360.0;
let bounds = spawn_box(fx, fy, fz, width, height);
if mob_spawn_rules(species, l, bx, pos[1], bz)
&& spawn_obstruction(species, l, bounds, &spawned, bx, pos[1], bz)
&& spawn_obstruction(species, l, bounds, None, bx, pos[1], bz)
{
let request = SpawnRequest {
entity: &entry.entity,
@@ -1321,9 +1323,8 @@ pub fn spawn_mobs_for_chunk_generation<L: NaturalLevel, R: RandomSource, Q: Rand
group_index,
reason: Reason::ChunkGeneration,
};
if let Some(result) = spawn(l, &request) {
if spawn(l, &request, rules) {
group_index += 1;
spawned.push(result.bounds);
trace(GenEvent::Spawn(entry.entity.clone(), [fx, fy, fz], yaw));
success = true;
}
+9 -1
View File
@@ -563,7 +563,15 @@ impl SpawnerExt for Spawner {
let rules_ok = data.custom_spawn_rules.is_some()
|| spawn_selection::mob_spawn_rules(s, l, own[0], own[1], own[2]);
if !rules_ok
|| !spawn_selection::spawn_obstruction(s, l, bounds, &added, own[0], own[1], own[2])
|| !spawn_selection::spawn_obstruction(
s,
l,
bounds,
Some(&added),
own[0],
own[1],
own[2],
)
{
continue;
}
@@ -1081,7 +1081,15 @@ impl TrialSpawnerExt for TrialSpawner {
width * scale,
height * scale,
);
if !spawn_selection::spawn_obstruction(species, l, bounds, &[], own[0], own[1], own[2]) {
if !spawn_selection::spawn_obstruction(
species,
l,
bounds,
Some(&[]),
own[0],
own[1],
own[2],
) {
return None;
}
let request = SpawnerSpawn {
+19
View File
@@ -19,6 +19,7 @@ use horizon_world::{
generator::{
GeneratorData, GeneratorWorker, OverworldGenerator, ProtoChunk, SurfaceSample,
},
structure::legacy::PieceRandom,
terrain::PreLightChunkStage,
},
};
@@ -221,6 +222,24 @@ impl VanillaTerrain {
pub fn set_spawn_conditions(&self, difficulty: u8, day_time: i64) {
self.structure_mobs.set_conditions(difficulty, day_time);
}
/// The random a chunk's generation steps draw from (`WorldGenRegion`).
pub fn region_random(&self, cx: i32, cz: i32) -> Box<dyn PieceRandom> {
self.generator.region_random(ChunkPos::new(cx, cz))
}
/// A mob chunk generation spawns, as the chunk saves it
/// (`StructureMobFinalizer::spawn_generated`).
pub fn spawn_generated(
&self,
kind: &str,
position: horizon_core::geometry::Vec3,
yaw: f32,
biome: &str,
random: &mut dyn PieceRandom,
group: &mut Option<crate::mobs::finalize::Group>,
) -> Option<horizon_world::nbt::Compound> {
self.structure_mobs
.spawn_generated(kind, position, yaw, biome, random, group)
}
/// A proto chunk's blocks, biomes, scheduled ticks and post-processing
/// marks as a column.
fn column(&self, chunk: &ProtoChunk) -> Chunk {
@@ -8,7 +8,7 @@ use crate::{
items::Items,
mobs::{
Mob, MobType, MobTypes,
finalize::{self, Ctx, FinalizeLevel, Reason, SpawnData, SpawnRegistries},
finalize::{self, Ctx, FinalizeLevel, Group, Reason, SpawnData, SpawnRegistries},
spawning::{LocalDifficulty, MOON_BRIGHTNESS},
},
};
@@ -135,6 +135,8 @@ fn run<R: RandomSource>(
difficulty: LocalDifficulty,
data: &SpawnData<'_>,
m: &mut Mob,
reason: Reason,
group: &mut Option<Group>,
) -> Vec<Mob> {
let mut cx = Ctx {
level,
@@ -144,7 +146,7 @@ fn run<R: RandomSource>(
created: Vec::new(),
jockey_rider: None,
};
cx.finalize(m, Reason::Structure, None);
*group = cx.finalize(m, reason, group.take());
if let Some(rider) = cx.jockey_rider {
debug_assert_eq!(rider.id, m.id);
*m = rider;
@@ -190,23 +192,78 @@ impl StructureMobs for StructureMobFinalizer {
structures: &[String],
random: &mut dyn PieceRandom,
) -> bool {
let Some(data) = self.data.get_or_init(|| {
let data = load(&self.root);
if data.is_none() {
log::error!("Horizon: no mob data, structure mobs are finalized when loaded");
}
data
}) else {
let Some(data) = self.data() else {
return false;
};
let mut raw = Tag::Compound(nbt.clone());
if let Tag::Compound(c) = &mut raw {
c.remove(&"horizon:finalize".into());
}
let Some(mut m) = entity_nbt::mob_from_nbt(&raw, &data.types, 1) else {
let Some(m) = entity_nbt::mob_from_nbt(&raw, &data.types, 1) else {
// Not a mob: vanilla finalizes only Mob instances.
return true;
};
let reason = Reason::Structure;
match self.finish(
data,
m,
Some(raw),
biome,
structures,
random,
reason,
&mut None,
) {
Some(saved) => {
*nbt = saved;
true
}
None => false,
}
}
}
impl StructureMobFinalizer {
fn data(&self) -> Option<&Data> {
self.data
.get_or_init(|| {
let data = load(&self.root);
if data.is_none() {
log::error!("Horizon: no mob data, structure mobs are finalized when loaded");
}
data
})
.as_ref()
}
/// A mob chunk generation spawns (`spawnOriginalMobs`): a new one of
/// `kind` finalized as a structure mob is, as the chunk saves it; `group`
/// carries a pack's group data from one member to the next.
pub fn spawn_generated(
&self,
kind: &str,
position: Vec3,
yaw: f32,
biome: &str,
random: &mut dyn PieceRandom,
group: &mut Option<Group>,
) -> Option<Compound> {
let data = self.data()?;
let m = Mob::new(1, data.types.get(kind)?.clone(), position, yaw);
let reason = Reason::ChunkGeneration;
self.finish(data, m, None, biome, &[], random, reason, group)
}
#[allow(clippy::too_many_arguments)]
fn finish(
&self,
data: &Data,
mut m: Mob,
raw: Option<Tag>,
biome: &str,
structures: &[String],
random: &mut dyn PieceRandom,
reason: Reason,
group: &mut Option<Group>,
) -> Option<Compound> {
let base = self.difficulty.load(Ordering::Relaxed);
let day_time = self.day_time.load(Ordering::Relaxed);
let mut level = Level {
@@ -228,21 +285,20 @@ impl StructureMobs for StructureMobFinalizer {
};
let any = random.as_any();
let created = if let Some(r) = any.downcast_mut::<LegacyRandom>() {
run(&mut level, r, difficulty, &spawn, &mut m)
run(&mut level, r, difficulty, &spawn, &mut m, reason, group)
} else if let Some(r) = any.downcast_mut::<XoroshiroRandom>() {
run(&mut level, r, difficulty, &spawn, &mut m)
run(&mut level, r, difficulty, &spawn, &mut m, reason, group)
} else {
log::error!("Horizon: unknown region random, structure mobs are finalized when loaded");
return false;
return None;
};
let mut mobs = vec![(m, Some(raw))];
let mut mobs = vec![(m, raw)];
mobs.extend(created.into_iter().map(|c| (c, None)));
let root = mobs
.iter()
.position(|(m, _)| m.extra.mount.is_none())
.unwrap_or(0);
*nbt = tree(&mobs, root, &self.dimension);
true
Some(tree(&mobs, root, &self.dimension))
}
}
+24 -11
View File
@@ -1334,6 +1334,17 @@ impl<R: RandomSource> Pipeline<R>
where
R::Positional: Clone + Send + Sync,
{
/// WorldGenRegion's level random, positioned at the center chunk.
fn region_random_at(&self, center: ChunkPos) -> R {
self.root
.from_hash_of("minecraft:worldgen_region_random")
.fork_positional()
.at(BlockPos::new(
center.x.wrapping_shl(4),
0,
center.z.wrapping_shl(4),
))
}
/// ChunkGenerator.getBaseHeight: the first free Y above the noise column
/// (with aquifers, without terrain adaptation) for a heightmap type.
fn base_height(&self, x: i32, z: i32, kind: HeightmapType) -> i32 {
@@ -1778,6 +1789,7 @@ trait SeededPipeline: Send + Sync {
fn forget_structure_starts(&self);
fn ring_positions(&self, worker: &mut WorkerState, set: &str) -> Option<Vec<ChunkPos>>;
fn spawn_position(&self) -> Result<[i32; 2], GeneratorError>;
fn region_random(&self, center: ChunkPos) -> Box<dyn super::structure::legacy::PieceRandom>;
fn locate(
&self,
worker: &mut WorkerState,
@@ -1816,17 +1828,7 @@ where
) -> Result<DecorateStats, GeneratorError> {
match &self.decoration {
Some(decoration) => {
let center = region[4].position();
// WorldGenRegion's level random, positioned at the center chunk.
let mut random = self
.root
.from_hash_of("minecraft:worldgen_region_random")
.fork_positional()
.at(BlockPos::new(
center.x.wrapping_shl(4),
0,
center.z.wrapping_shl(4),
));
let mut random = self.region_random_at(region[4].position());
decoration.decorate(
&self.data,
self.seed,
@@ -1915,6 +1917,9 @@ where
set,
)
}
fn region_random(&self, center: ChunkPos) -> Box<dyn super::structure::legacy::PieceRandom> {
Box::new(self.region_random_at(center))
}
fn spawn_position(&self) -> Result<[i32; 2], GeneratorError> {
if let Some(position) = self.spawn_position.get() {
return Ok(*position);
@@ -2551,6 +2556,14 @@ impl OverworldGenerator {
pub fn spawn_position(&self) -> Result<[i32; 2], GeneratorError> {
self.pipeline.spawn_position()
}
/// A new `WorldGenRegion` random centred on the chunk: what its generation
/// steps draw from (`level.getRandom()`).
pub fn region_random(
&self,
center: ChunkPos,
) -> Box<dyn super::structure::legacy::PieceRandom> {
self.pipeline.region_random(center)
}
/// Create per-thread state. Workers are cheap; keep one per thread.
pub fn worker(&self) -> GeneratorWorker {
let data = self.pipeline.data();
@@ -246,7 +246,7 @@ public final class VanillaFinalizeSpawnOracle {
Map.entry("jump_strength", Attributes.JUMP_STRENGTH));
static final List<Map.Entry<String, MemoryModuleType<?>>> MEMORIES = List.of(
Map.entry("long_jump_cooldown", MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS), Map.entry("ram_cooldown", MemoryModuleType.RAM_COOLDOWN_TICKS),
Map.entry("long_jump_cooling_down", MemoryModuleType.LONG_JUMP_COOLDOWN_TICKS), Map.entry("ram_cooldown_ticks", MemoryModuleType.RAM_COOLDOWN_TICKS),
Map.entry("attack_target_cooldown", MemoryModuleType.ATTACK_TARGET_COOLDOWN), Map.entry("hunted_recently", MemoryModuleType.HUNTED_RECENTLY),
Map.entry("dig_cooldown", MemoryModuleType.DIG_COOLDOWN), Map.entry("is_emerging", MemoryModuleType.IS_EMERGING),
Map.entry("home", MemoryModuleType.HOME));
@@ -171,6 +171,8 @@ public final class VanillaNaturalSpawnOracle {
int tick = -1;
List<String> log;
boolean logColumns;
// A WorldGenRegion sees no entities (getEntities is empty).
boolean generationRegion;
int next = 1;
@Override public RegistryAccess registryAccess() { return registries; }
@Override public Difficulty getDifficulty() { return difficulty; }
@@ -281,11 +283,13 @@ public final class VanillaNaturalSpawnOracle {
@Override public Iterable<Entity> getAllEntities() { return entities; }
@Override public List<Entity> getEntities(Entity except, AABB bb, Predicate<? super Entity> selector) {
List<Entity> out = new ArrayList<>();
if (generationRegion) return out;
for (Entity e : entities) if (e != except && e.getBoundingBox().intersects(bb) && selector.test(e)) out.add(e);
return out;
}
@Override public <T extends Entity> List<T> getEntities(EntityTypeTest<Entity, T> type, AABB bb, Predicate<? super T> selector) {
List<T> out = new ArrayList<>();
if (generationRegion) return out;
for (Entity e : entities) {
T t = type.tryCast(e);
if (t != null && e.getBoundingBox().intersects(bb) && selector.test(t)) out.add(t);
@@ -811,11 +815,13 @@ public final class VanillaNaturalSpawnOracle {
vaultCase(level, c, access, blockLookup, mainRandom, log);
} else if (mode.equals("gen")) {
level.logColumns = true;
level.generationRegion = true;
int n = level.entities.size();
RandomSource random = RandomSource.create(Long.parseLong(c.getOrDefault("gseed", "0")));
BlockPos sourcePos = new BlockPos(chunk.at.getMinBlockX(), level.getMaxY(), chunk.at.getMinBlockZ());
NaturalSpawner.spawnMobsForChunkGeneration(level, sourcePos, chunk.at, random);
level.logColumns = false;
level.generationRegion = false;
for (Entity e : level.entities.subList(n, level.entities.size()))
if (e.getVehicle() == null)
log.add("spawn " + BuiltInRegistries.ENTITY_TYPE.getKey(e.getType()) + " " + bits(e.getX()) + " " + bits(e.getY()) + " " + bits(e.getZ()) + " " + yaw(e));