Merge branch 'parity-base' into parity/saves

# Conflicts:
#	README.md
This commit is contained in:
Ludovic Marie
2026-10-11 17:51:57 +02:00
14 changed files with 3716 additions and 382 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ round-trip harness has not been rerun.
| Lighting | 1 | None: saved and relit light matches vanilla, slabs and stairs included |
| World generation | 1 | None: 2,352 whole chunks over 225 structure sites, in all three dimensions, identical to vanilla |
| Region files and saves | 0.75 | Saves round-trip with vanilla, and worlds from 1.18.2 to 26.2 upgrade as vanilla's DataFixers do over 22 sample worlds; worlds older than 1.18.2 are not upgraded |
| Entities and physics | 0.75 | Attributes and dimensions match; AI, loot and spawn rules not compared; drowned, villagers, traders and the wither unverified |
| Entities and physics | 0.75 | Attributes, dimensions, death loot, spawn rules and natural spawning (15,500 NaturalSpawner cases) match; AI not compared; drowned, villagers, traders and the wither unverified |
| Interaction and inventory | 1 | None: menu clicks match over 6,000 click sequences |
| Menus, HUD and chat | 0.75 | Commands parse as vanilla over 39,685 inputs, but /item modify, /spectate, /waypoint and /dialog do not run; no tab list or world border rendering |
| Audio | 0.75 | Sound engine, music and ambience not compared with vanilla |
@@ -49,7 +49,7 @@ round-trip harness has not been rerun.
1. Upgrade saves older than 1.18.2 (the DataFixers before DataVersion 2970).
2. Draw every item through the item model pipeline, and add the resource pack screen.
3. An audio harness: sound engine attenuation, weighted sound events, music and ambience.
4. Mob AI, loot and spawn-rule harnesses, and an oracle for drowned, villagers, traders and the wither.
4. A mob AI harness, and an oracle for drowned, villagers, traders and the wither.
5. The missing commands and dispenser behaviours, the tab list and world border rendering.
6. Test LAN hosting with a vanilla client, and add a dedicated server mode.
@@ -0,0 +1,688 @@
//! Natural spawning decisions for differential tests against vanilla
//! (tools/test_natural_spawn_parity.py): reads the box-world cases of
//! `VanillaNaturalSpawnOracle` and prints what `mobs::spawn_selection` does in
//! the same order and format.
//!
//! The oracle runs `finalizeSpawn` with a fixed random (nextInt 0, nextFloat
//! 0.75); `neutral_box` is the box that leaves a mob with: polar bears,
//! ocelots and rabbits after the first of their group are babies (chance
//! 1.0), foxes and axolotls from the third, sulfur cubes have size 2, salmon
//! are small, slimes and magma cubes keep size 1, tropical fish school.
//!
//! usage: natural_spawn_fixture RESOURCES INPUT OUTPUT
use horizon_client::{
assets::Assets,
biome::{Biomes, SpawnEntry},
blocks::BlockFacts,
collisions::{self, LookupCollisions, no_collision_empty},
items::Items,
mobs::{
MobTypes, Species,
data::MobData,
spawn_rules::{Dimension, SpawnLevel},
spawn_selection::{
self, Category, Counted, Event, GenEvent, Heightmap, NaturalLevel, SpawnCost,
SpawnPlayer, SpawnRequest, SpawnState, Spawned, StructureAt, StructureSpawns,
spawn_box,
},
species::{self, Baby},
},
};
use horizon_core::{
geometry::Aabb,
random::{LegacyRandom, RandomSource},
};
use horizon_world::{
block_collision::BlockCollisionRegistry, chunk::BlockState, registry::BlockStateId,
};
use serde_json::Value;
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap, HashSet},
fmt::Write as _,
fs,
path::Path,
};
#[derive(Clone, Copy)]
struct Bx([i32; 6]);
impl Bx {
fn parse(s: &str) -> Self {
let v: Vec<i32> = s.split(',').map(|n| n.parse().unwrap_or(0)).collect();
Self([v[0], v[1], v[2], v[3], v[4], v[5]])
}
fn has(&self, x: i32, y: i32, z: i32) -> bool {
let b = self.0;
x >= b[0] && x <= b[3] && y >= b[1] && y <= b[4] && z >= b[2] && z <= b[5]
}
}
struct Start {
id: String,
pieces: Vec<Bx>,
/// The start's bounding box (pieces encapsulated, inflated by 12 for
/// terrain adaptation).
bounds: Bx,
}
struct Shared<'a> {
facts: &'a BlockFacts,
data: &'a MobData,
block_tags: &'a HashMap<String, HashSet<String>>,
types: &'a MobTypes,
biomes: &'a Biomes,
structures: &'a StructureSpawns,
collision: &'a BlockCollisionRegistry<'a>,
air: BlockStateId,
}
struct World<'a> {
s: &'a Shared<'a>,
blocks: Vec<(Bx, BlockStateId)>,
lights: Vec<(Bx, i32, i32)>,
biomes: Vec<(Bx, String)>,
biome: String,
sky: i32,
light: i32,
darken: i32,
thunder: bool,
sea: i32,
seed: i64,
peaceful: bool,
dimension: Dimension,
dimension_id: String,
slime: f32,
starts: Vec<Start>,
players: Vec<([f64; 3], String)>,
respawn: Option<[i32; 3]>,
unloaded: HashSet<(i32, i32)>,
spawn_mobs: bool,
/// Boxes of living entities (case entities, then spawned mobs).
entities: RefCell<Vec<Aabb>>,
}
impl World<'_> {
fn biome_at(&self, x: i32, y: i32, z: i32) -> &str {
self.biomes
.iter()
.rev()
.find(|(b, _)| b.has(x, y, z))
.map_or(&self.biome, |(_, n)| n)
}
fn biome_info(&self, x: i32, y: i32, z: i32) -> Option<&horizon_client::biome::BiomeInfo> {
let i = *self.s.biomes.index.get(self.biome_at(x, y, z))?;
Some(self.s.biomes.get(i))
}
fn lights_at(&self, x: i32, y: i32, z: i32) -> (i32, i32) {
self.lights
.iter()
.rev()
.find(|(b, ..)| b.has(x, y, z))
.map_or((self.sky, self.light), |(_, s, b)| (*s, *b))
}
}
impl SpawnLevel for World<'_> {
fn state(&self, x: i32, y: i32, z: i32) -> BlockStateId {
self.blocks
.iter()
.rev()
.find(|(b, _)| b.has(x, y, z))
.map_or(self.s.air, |(_, s)| *s)
}
fn facts(&self) -> &BlockFacts {
self.s.facts
}
fn mob_data(&self) -> &MobData {
self.s.data
}
fn block_tag(&self, tag: &str, block: &str) -> bool {
self.s
.block_tags
.get(tag)
.is_some_and(|s| s.contains(block))
}
fn biome_tag(&self, tag: &str, x: i32, y: i32, z: i32) -> bool {
self.s
.data
.biome_tags
.get(tag)
.is_some_and(|s| s.contains(self.biome_at(x, y, z)))
}
fn sky_light(&self, x: i32, y: i32, z: i32) -> i32 {
self.lights_at(x, y, z).0
}
fn block_light(&self, x: i32, y: i32, z: i32) -> i32 {
self.lights_at(x, y, z).1
}
fn sky_darken(&self) -> i32 {
self.darken
}
fn thundering(&self) -> bool {
self.thunder
}
fn sea_level(&self) -> i32 {
self.sea
}
fn world_surface(&self, x: i32, z: i32) -> i32 {
self.heightmap(Heightmap::WorldSurface, x, z)
}
fn peaceful(&self) -> bool {
self.peaceful
}
fn dimension(&self) -> Dimension {
self.dimension
}
fn surface_slime_chance(&self, _: i32, _: i32, _: i32) -> f32 {
self.slime
}
fn world_seed(&self) -> i64 {
self.seed
}
fn player_near(&self, x: f64, y: f64, z: f64, distance: f64) -> bool {
self.players.iter().any(|(p, mode)| {
let d = (p[0] - x).powi(2) + (p[1] - y).powi(2) + (p[2] - z).powi(2);
(mode == "survival" || mode == "adventure") && d < distance * distance
})
}
}
impl NaturalLevel for World<'_> {
fn min_y(&self) -> i32 {
if self.dimension == Dimension::Overworld {
-64
} else {
0
}
}
fn max_y(&self) -> i32 {
if self.dimension == Dimension::Overworld {
319
} else {
255
}
}
fn ambient_light(&self) -> f32 {
match self.dimension {
Dimension::Overworld => 0.0,
Dimension::Nether => 0.1,
Dimension::End => 0.25,
}
}
fn has_ceiling(&self) -> bool {
self.dimension == Dimension::Nether
}
fn mob_type(&self, entity: &str) -> Option<(Species, f32, f32, Category)> {
let t = self.s.types.types.get(entity)?;
Some((
t.species,
t.width as f32,
t.height as f32,
Category::from_name(&t.category),
))
}
fn biome_spawns(&self, x: i32, y: i32, z: i32) -> Option<&HashMap<String, Vec<SpawnEntry>>> {
self.biome_info(x, y, z).map(|b| &b.spawns)
}
fn spawn_cost(&self, entity: &str, x: i32, y: i32, z: i32) -> Option<SpawnCost> {
self.biome_info(x, y, z)?.spawn_costs.get(entity).copied()
}
fn creature_probability(&self, x: i32, y: i32, z: i32) -> f32 {
self.biome_info(x, y, z)
.map_or(0.1, |b| b.creature_probability)
}
fn structures_at(&self, x: i32, y: i32, z: i32) -> Vec<StructureAt> {
let (cx, cz) = (x >> 4, z >> 4);
let mut out: Vec<StructureAt> = Vec::new();
for start in &self.starts {
let b = start.bounds.0;
// Referenced: the start's box reaches the chunk's columns.
if b[3] < cx * 16 || b[0] > cx * 16 + 15 || b[5] < cz * 16 || b[2] > cz * 16 + 15 {
continue;
}
let in_piece = start.pieces.iter().any(|p| p.has(x, y, z));
let in_bounds = start.bounds.has(x, y, z);
match out.iter_mut().find(|s| s.id == start.id) {
Some(s) => {
s.in_piece |= in_piece;
s.in_bounds |= in_bounds;
}
None => out.push(StructureAt {
id: start.id.clone(),
in_piece,
in_bounds,
}),
}
}
out
}
fn structure_spawns(&self) -> &StructureSpawns {
self.s.structures
}
fn nearest_player_distance_sqr(&self, x: f64, y: f64, z: f64) -> Option<f64> {
let mut best: Option<f64> = None;
for (p, mode) in &self.players {
if mode == "spectator" {
continue;
}
let d = [p[0] - x, p[1] - y, p[2] - z];
let d = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
if best.is_none_or(|b| d < b) {
best = Some(d);
}
}
best
}
fn respawn(&self) -> Option<[i32; 3]> {
self.respawn
}
fn can_spawn_in_chunk(&self, cx: i32, cz: i32) -> bool {
!self.unloaded.contains(&(cx, cz))
}
fn no_collision(&self, bounds: Aabb) -> bool {
let lookup = |x: i32, y: i32, z: i32| self.state(x, y, z);
let mut world = LookupCollisions {
block: &lookup,
binding: self.s.collision,
};
no_collision_empty(&mut world, bounds)
}
fn unobstructed(&self, bounds: Aabb) -> bool {
!self.entities.borrow().iter().any(|b| b.intersects(bounds))
}
fn spawn_mobs(&self) -> bool {
self.spawn_mobs
}
fn heightmap(&self, kind: Heightmap, x: i32, z: i32) -> i32 {
(self.min_y()..=self.max_y())
.rev()
.find(|&y| spawn_selection::heightmap_blocks(self, kind, x, y, z))
.map_or(self.min_y(), |y| y + 1)
}
fn pathfindable_land(&self, x: i32, y: i32, z: i32) -> bool {
self.s.data.state(self.state(x, y, z)).flags & horizon_client::mobs::data::LAND != 0
}
}
/// The box a mob is left with by `finalizeSpawn` under the oracle's fixed random.
fn neutral_box(l: &World, r: &SpawnRequest) -> Aabb {
let t = &l.s.types.types[r.entity];
let (mut w, mut h) = (t.width as f32, t.height as f32);
let baby = match r.species {
Species::PolarBear | Species::Ocelot | Species::Rabbit => r.group_index > 0,
Species::Fox | Species::Axolotl => r.group_index >= 2,
_ => false,
};
if baby {
match species::def(r.species).baby {
Baby::Abs(bw, bh, _) => (w, h) = (bw as f32, bh as f32),
Baby::Scale(s, _) => (w, h) = (w * s as f32, h * s as f32),
Baby::None => {}
}
}
match r.species {
Species::SulfurCube => (w, h) = (w * 2.0, h * 2.0),
Species::Salmon => (w, h) = (w * 0.5, h * 0.5),
_ => {}
}
let [x, y, z] = r.position;
spawn_box(x, y, z, w, h)
}
fn parse_state(spec: &str) -> BlockState {
let (name, props) = match spec.split_once('[') {
Some((n, rest)) => (n, rest.trim_end_matches(']')),
None => (spec, ""),
};
BlockState {
name: name.to_owned(),
properties: props
.split(',')
.filter(|p| !p.is_empty())
.filter_map(|p| p.split_once('='))
.map(|(k, v)| (k.to_owned(), v.to_owned()))
.collect(),
}
}
fn bits(v: f64) -> String {
format!("{:x}", v.to_bits())
}
/// The yaw as the oracle prints it: a cube mob grown by `finalizeSpawn`
/// (sulfur cubes, size 2 under the fixed random) takes its constructor's
/// head rotation from the entity's unseeded random.
fn yaw_text(entity: &str, v: f32) -> String {
if entity == "minecraft:sulfur_cube" {
"head".to_owned()
} else {
format!("{:x}", v.to_bits())
}
}
fn main() -> horizon_client::Result<()> {
let args: Vec<String> = std::env::args().collect();
let root = Path::new(
args.get(1)
.ok_or("usage: natural_spawn_fixture RESOURCES INPUT OUTPUT")?,
);
let input = fs::read_to_string(args.get(2).ok_or("missing INPUT")?)?;
let assets = Assets::load(root)?;
let registry = assets.registry;
let facts = BlockFacts::load(root, registry)?;
let data = MobData::load(root, &facts);
let items = Items::tags_only(root)?;
let types = MobTypes::load(root)?;
let biomes = Biomes::load(root)?;
let structures = StructureSpawns::load(root);
let collision = collisions::load(root, registry)?;
let shared = Shared {
facts: &facts,
data: &data,
block_tags: &items.block_tags,
types: &types,
biomes: &biomes,
structures: &structures,
collision: &collision,
air: facts.air,
};
let mut adaptation: HashMap<String, bool> = HashMap::new();
let mut out = String::new();
for raw in input.lines() {
if raw.trim().is_empty() {
continue;
}
let mut c = BTreeMap::new();
let mut name = "";
for token in raw.split_whitespace() {
match token.split_once('=') {
Some((k, v)) => {
c.insert(k, v);
}
None => name = token,
}
}
let get =
|k: &str, d: &'static str| -> String { c.get(k).copied().unwrap_or(d).to_owned() };
let indexed = |prefix: &str| -> Vec<&str> {
(0..)
.map_while(|i| c.get(format!("{prefix}{i}").as_str()).copied())
.collect()
};
let mut blocks = Vec::new();
for k in indexed("k") {
let (b, s) = k.split_once('/').ok_or("bad block box")?;
blocks.push((Bx::parse(b), registry.resolve(&parse_state(s))?));
}
let lights = indexed("l")
.into_iter()
.filter_map(|l| {
let (b, v) = l.split_once('/')?;
let (s, k) = v.split_once(',')?;
Some((Bx::parse(b), s.parse().ok()?, k.parse().ok()?))
})
.collect();
let biome_boxes = indexed("b")
.into_iter()
.filter_map(|l| {
let (b, n) = l.split_once('/')?;
Some((Bx::parse(b), n.to_owned()))
})
.collect();
let mut starts = Vec::new();
for s in indexed("s") {
let (id, pieces) = s.split_once('/').ok_or("bad structure")?;
let pieces: Vec<Bx> = pieces.split(';').map(Bx::parse).collect();
let mut b = pieces[0].0;
for p in &pieces[1..] {
for i in 0..3 {
b[i] = b[i].min(p.0[i]);
b[i + 3] = b[i + 3].max(p.0[i + 3]);
}
}
let adapts = *adaptation.entry(id.to_owned()).or_insert_with(|| {
let path = root.join(format!(
"data/minecraft/worldgen/structure/{}.json",
id.trim_start_matches("minecraft:")
));
fs::read(path)
.ok()
.and_then(|b| serde_json::from_slice::<Value>(&b).ok())
.and_then(|v| v["terrain_adaptation"].as_str().map(|a| a != "none"))
.unwrap_or(false)
});
if adapts {
for i in 0..3 {
b[i] -= 12;
b[i + 3] += 12;
}
}
starts.push(Start {
id: id.to_owned(),
pieces,
bounds: Bx(b),
});
}
let players = indexed("p")
.into_iter()
.map(|p| {
let mut parts = p.split('/');
let v: Vec<f64> = parts
.next()
.unwrap_or("")
.split(',')
.map(|n| n.parse().unwrap_or(0.0))
.collect();
(
[v[0], v[1], v[2]],
parts.next().unwrap_or("survival").to_owned(),
)
})
.collect::<Vec<_>>();
let dimension_id = get("dim", "minecraft:overworld");
let dimension = match dimension_id.as_str() {
"minecraft:the_nether" => Dimension::Nether,
"minecraft:the_end" => Dimension::End,
_ => Dimension::Overworld,
};
let respawn = {
let r = get("respawn", "0,-1000,0/minecraft:overworld");
let (p, d) = r.split_once('/').ok_or("bad respawn")?;
let v: Vec<i32> = p.split(',').map(|n| n.parse().unwrap_or(0)).collect();
(d == dimension_id).then_some([v[0], v[1], v[2]])
};
let unloaded = c
.get("unloaded")
.map(|u| {
u.split(';')
.filter_map(|p| {
let (x, z) = p.split_once(',')?;
Some((x.parse().ok()?, z.parse().ok()?))
})
.collect()
})
.unwrap_or_default();
// Case entities: (type, position, persistent).
let mut case_entities = Vec::new();
for e in indexed("e") {
let parts: Vec<&str> = e.split('/').collect();
let v: Vec<f64> = parts[1]
.split(',')
.map(|n| n.parse().unwrap_or(0.0))
.collect();
// EntityType.create makes no monsters in peaceful.
if get("difficulty", "normal") == "peaceful"
&& types.types.get(parts[0]).is_some_and(|t| {
!horizon_client::mobs::spawn_rules::allowed_in_peaceful(t.species)
})
{
continue;
}
case_entities.push((
parts[0].to_owned(),
[v[0], v[1], v[2]],
parts.get(2) == Some(&"p"),
));
}
let world = World {
s: &shared,
blocks,
lights,
biomes: biome_boxes,
biome: get("biome", "minecraft:plains"),
sky: get("sky", "15").parse()?,
light: get("light", "0").parse()?,
darken: get("darken", "0").parse()?,
thunder: get("thunder", "0") == "1",
sea: get("sea", "63").parse()?,
seed: get("wseed", "0").parse()?,
peaceful: get("difficulty", "normal") == "peaceful",
dimension,
dimension_id: dimension_id.clone(),
slime: get("slime", "0.0").parse()?,
starts,
players,
respawn,
unloaded,
spawn_mobs: get("rules", "1") != "0",
entities: RefCell::new(
case_entities
.iter()
.filter_map(|(t, p, _)| {
let t = types.types.get(t)?;
Some(spawn_box(p[0], p[1], p[2], t.width as f32, t.height as f32))
})
.collect(),
),
};
let _ = &world.dimension_id;
let chunk = {
let v: Vec<i32> = get("chunk", "0,0")
.split(',')
.map(|n| n.parse().unwrap_or(0))
.collect();
(v[0], v[1])
};
let mut random = LegacyRandom::new(get("seed", "0").parse()?);
let mut log: Vec<String> = Vec::new();
let mut spawn = |l: &World, r: &SpawnRequest| -> Option<Spawned> {
let bounds = neutral_box(l, r);
l.entities.borrow_mut().push(bounds);
Some(Spawned {
bounds,
school: true,
})
};
let mode = get("mode", "chunk");
if mode == "gen" {
let mut gen_random = LegacyRandom::new(get("gseed", "0").parse()?);
// The level random of the spawn predicates: no creature's predicate
// draws from it (the oracle's answers fixed values).
let mut rules = LegacyRandom::new(0);
let mut spawns = Vec::new();
let source = [chunk.0 * 16, world.max_y(), chunk.1 * 16];
spawn_selection::spawn_mobs_for_chunk_generation(
&world,
source,
chunk,
&mut gen_random,
&mut rules,
&mut spawn,
&mut |e| match e {
GenEvent::Column(x, z) => log.push(format!("col {x} {z}")),
GenEvent::Spawn(t, p, yaw) => spawns.push(format!(
"spawn {t} {} {} {} {}",
bits(p[0]),
bits(p[1]),
bits(p[2]),
yaw_text(&t, yaw)
)),
},
);
log.extend(spawns);
log.push(format!(
"next {}",
gen_random.next_int_bound(1000).unwrap_or(0)
));
} else {
let counted: Vec<Counted> = case_entities
.iter()
.filter_map(|(t, p, persistent)| {
let kind = types.types.get(t)?;
Some(Counted {
entity: t,
category: Category::from_name(&kind.category),
block: [
p[0].floor() as i32,
p[1].floor() as i32,
p[2].floor() as i32,
],
is_mob: true,
persistent: *persistent,
})
})
.collect();
let players = world
.players
.iter()
.map(|(p, mode)| SpawnPlayer {
position: *p,
spectator: mode == "spectator",
})
.collect();
let mut state = SpawnState::new(
&world,
get("chunkcount", "289").parse()?,
players,
&counted,
|cx, cz| !world.unloaded.contains(&(cx, cz)),
);
let mut trace = |e: Event| {
log.push(match e {
Event::Categories(list) => format!(
"cats {}",
list.iter().map(|c| c.name()).collect::<Vec<_>>().join(",")
),
Event::Local(c, ok) => format!("local {} {ok}", c.name()),
Event::Start(c, p) => format!("start {} {} {} {}", c.name(), p[0], p[1], p[2]),
Event::Try(x, z) => format!("try {x} {z}"),
Event::Test(t, p, ok) => format!("test {t} {} {} {} {ok}", p[0], p[1], p[2]),
Event::Spawn(t, p, yaw) => format!(
"spawn {t} {} {} {} {}",
bits(p[0]),
bits(p[1]),
bits(p[2]),
yaw_text(&t, yaw)
),
})
};
if mode == "pos" {
let category = Category::from_name(&get("cat", "monster"));
let v: Vec<i32> = get("start", "0,0,0")
.split(',')
.map(|n| n.parse().unwrap_or(0))
.collect();
spawn_selection::spawn_category_for_position(
&world,
&mut random,
&mut state,
category,
chunk,
[v[0], v[1], v[2]],
&mut spawn,
&mut trace,
);
} else {
let categories = state
.filtered_categories(get("enemies", "1") == "1", get("persistent", "1") == "1");
trace(Event::Categories(categories.clone()));
spawn_selection::spawn_for_chunk(
&world,
&mut random,
&mut state,
&categories,
chunk,
&mut spawn,
&mut trace,
);
}
log.push(format!("next {}", random.next_int_bound(1000).unwrap_or(0)));
}
let _ = writeln!(out, "{name} {}", log.join(" ; "));
}
fs::write(args.get(3).ok_or("missing OUTPUT")?, out)?;
Ok(())
}
+20 -26
View File
@@ -17,11 +17,16 @@ pub struct SpawnEntry {
pub weight: u32,
pub min: u32,
pub max: u32,
/// A `ConstantInt` count (sampling draws nothing).
pub constant: bool,
}
#[derive(Clone, Debug)]
pub struct BiomeInfo {
/// `natural_mob_spawns` by category (creature, monster, ...).
pub spawns: HashMap<String, Vec<SpawnEntry>>,
/// `natural_mob_spawns` spawn costs by entity type.
pub spawn_costs: HashMap<String, crate::mobs::spawn_selection::SpawnCost>,
/// The `gameplay/creature_world_gen_spawn_probability` attribute.
pub creature_probability: f32,
pub name: String,
pub temperature: f32,
@@ -103,37 +108,26 @@ impl Biomes {
_ => GrassModifier::None,
};
let mut spawns: HashMap<String, Vec<SpawnEntry>> = HashMap::new();
let categories = &attributes["minecraft:gameplay/natural_mob_spawns"]["argument"]["spawns_by_category"];
let natural = &attributes["minecraft:gameplay/natural_mob_spawns"];
// A modifier object ({argument, modifier: overlay}) or the plain value;
// the overlay over the dimension's empty default is the biome's value.
let natural = if natural["argument"].is_object() {
&natural["argument"]
} else {
natural
};
let categories = &natural["spawns_by_category"];
for (category, entries) in categories.as_object().into_iter().flatten() {
let list = entries
.as_array()
.into_iter()
.flatten()
.filter_map(|e| {
let (min, max) = match &e["count"] {
Value::Number(n) => {
let n = n.as_u64().unwrap_or(1) as u32;
(n, n)
}
c => (
c["min_inclusive"].as_u64().unwrap_or(1) as u32,
c["max_inclusive"].as_u64().unwrap_or(1) as u32,
),
};
Some(SpawnEntry {
entity: e["type"].as_str()?.to_owned(),
weight: e["weight"].as_u64().unwrap_or(1) as u32,
min,
max,
})
})
.collect();
let list = crate::mobs::spawn_selection::parse_entries(entries);
spawns.insert(category.clone(), list);
}
list.push(BiomeInfo {
spawns,
creature_probability: v["creature_spawn_probability"].as_f64().unwrap_or(0.1)
as f32,
spawn_costs: crate::mobs::spawn_selection::parse_costs(&natural["spawn_costs"]),
creature_probability:
attributes["minecraft:gameplay/creature_world_gen_spawn_probability"]
.as_f64()
.unwrap_or(0.1) as f32,
temperature,
downfall,
precipitation: v["has_precipitation"].as_bool().unwrap_or(true),
+105
View File
@@ -78,6 +78,23 @@ impl SceneCollisions<'_, '_> {
)?)
}
}
/// `noCollision(null, box)`: block collisions under the empty collision
/// context, as entity-less queries such as spawn checks use.
pub fn no_collision_empty<W, E>(world: &mut W, bounds: Aabb) -> bool
where
W: BlockCollisionWorld<Error = E> + EntityMovementWorld<Error = E>,
{
no_world_collision(
world,
CollisionCheckRequest {
entity: EntityIdentity(0),
context: CollisionContext::empty(false),
bounds,
candidate_budget: 1_000_000,
},
)
.unwrap_or(false)
}
impl BlockCollisionWorld for SceneCollisions<'_, '_> {
type Error = io::Error;
type Chunk = ChunkPos;
@@ -166,3 +183,91 @@ impl PowderSnowWorld for SceneCollisions<'_, '_> {
Ok(false)
}
}
/// Collision queries over any block lookup (test worlds without chunks):
/// every chunk is present, nothing is suffocating, no entities collide.
pub struct LookupCollisions<'w, 'r> {
pub block: &'w dyn Fn(i32, i32, i32) -> horizon_world::registry::BlockStateId,
pub binding: &'w BlockCollisionRegistry<'r>,
}
impl BlockCollisionWorld for LookupCollisions<'_, '_> {
type Error = io::Error;
type Chunk = ChunkPos;
fn collision_chunk(&mut self, p: ChunkPos) -> io::Result<Option<ChunkPos>> {
Ok(Some(p))
}
fn collision_block(&mut self, _: ChunkPos, p: BlockPos) -> io::Result<CollisionBlock> {
self.binding.collision_block((self.block)(p.x, p.y, p.z))
}
fn is_suffocating(&mut self, _: ChunkPos, _: CollisionBlock, _: BlockPos) -> io::Result<bool> {
Ok(false)
}
fn context_shape(
&mut self,
block: CollisionBlock,
p: BlockPos,
context: &CollisionContext<'_>,
) -> io::Result<VoxelShape> {
self.binding
.collision_shape_with_context(self, block.state, p, context)
.map_err(io::Error::other)
}
}
impl EntityMovementWorld for LookupCollisions<'_, '_> {
type Error = io::Error;
fn entity_collisions(&mut self, _: EntityIdentity, _: Aabb) -> io::Result<Vec<VoxelShape>> {
Ok(Vec::new())
}
fn movement_border(&mut self, _: EntityIdentity, _: Aabb) -> io::Result<Option<VoxelShape>> {
Ok(None)
}
}
impl DynamicSupportWorld for LookupCollisions<'_, '_> {
type Error = io::Error;
fn shulker_is_closed(&mut self, _: [i32; 3]) -> io::Result<Option<bool>> {
Ok(None)
}
fn moving_piston_shape(&mut self, _: [i32; 3]) -> io::Result<Option<VoxelShape>> {
Ok(None)
}
}
impl DynamicCollisionWorld for LookupCollisions<'_, '_> {
fn shulker_collision_state(
&mut self,
_: [i32; 3],
) -> io::Result<Option<ShulkerCollisionState>> {
Ok(None)
}
}
impl LiquidCollisionWorld for LookupCollisions<'_, '_> {
type Error = io::Error;
fn entity_liquid_shape(&mut self, _: EntityIdentity) -> io::Result<VoxelShape> {
Ok(VoxelShape::empty())
}
fn fluid(&mut self, p: BlockPos) -> io::Result<FluidState> {
FluidState::from_block(
self.binding
.registry()
.state((self.block)(p.x, p.y, p.z))
.ok_or_else(|| io::Error::other("fluid state outside registry"))?,
)
}
fn entity_can_stand(&mut self, _: EntityIdentity, _: FluidState) -> io::Result<bool> {
Ok(false)
}
}
impl PowderSnowWorld for LookupCollisions<'_, '_> {
type Error = io::Error;
fn powder_snow_entity(&mut self, _: EntityIdentity) -> io::Result<PowderSnowEntity> {
Ok(PowderSnowEntity {
fall_distance: 0.0,
falling_block: false,
})
}
fn powder_snow_walkable_tag(&mut self, _: EntityIdentity) -> io::Result<bool> {
Ok(false)
}
fn feet_are_leather_boots(&mut self, _: EntityIdentity) -> io::Result<bool> {
Ok(false)
}
}
+1 -4
View File
@@ -52,6 +52,7 @@ mod mount_menu;
mod mounts;
mod music;
mod names;
mod natural_spawning;
mod nautili;
mod options;
mod packs;
@@ -755,9 +756,6 @@ struct Game<'a> {
adv_ui: advancements_screen::AdvUi,
camera_type: perspective::CameraType,
fangs: fangs::FangState,
/// Structure `spawn_overrides`: structure -> category -> (piece only, spawns).
spawn_overrides:
HashMap<String, HashMap<String, (bool, Vec<horizon_client::biome::SpawnEntry>)>>,
/// The local player as a model (third person, the inventory).
player_model: Option<Mob>,
}
@@ -8133,7 +8131,6 @@ pub fn run() -> Result<()> {
adv_ui: Default::default(),
camera_type: Default::default(),
fangs: Default::default(),
spawn_overrides: HashMap::new(),
player_model: None,
};
game.ui.vanilla_root = vanilla_root;
-269
View File
@@ -37,17 +37,6 @@ pub(super) struct Creatures {
/// The wandering trader spawner's clock.
pub trader: super::wandering_trader::Spawner,
}
/// MobCategory caps (maxInstancesPerChunk with the 17x17 spawnable chunk area).
const CATEGORIES: [(&str, usize); 7] = [
("monster", 70),
("creature", 10),
("ambient", 15),
("axolotls", 5),
("underground_water_creature", 5),
("water_creature", 5),
("water_ambient", 20),
];
struct ProjWorld<'a, 'w> {
w: &'a MobWorld<'w>,
targets: Vec<(Who, Aabb)>,
@@ -355,99 +344,6 @@ impl Game<'_> {
}
horizon_client::chunk::MIN_Y
}
fn weighted_entry(
&mut self,
entries: &[horizon_client::biome::SpawnEntry],
) -> Option<horizon_client::biome::SpawnEntry> {
let total: u32 = entries.iter().map(|e| e.weight).sum();
if total == 0 {
return None;
}
let mut r = self.world.random.next_int_bound(total as i32).ok()? as u32;
for e in entries {
if r < e.weight {
return Some(e.clone());
}
r -= e.weight;
}
None
}
/// NaturalSpawner.spawnMobsForChunkGeneration (creature packs at generation).
pub(super) fn spawn_generation_creatures(&mut self, cx: i32, cz: i32) {
let Some(chunk) = self.world.chunks.get(&(cx, cz)).cloned() else {
return;
};
let biome = self.res.biomes.get(chunk.biome(8, 64, 8)).clone();
let Some(entries) = biome.spawns.get("creature").cloned() else {
return;
};
let (xo, zo) = (cx * 16, cz * 16);
while self.world.random.next_float() < biome.creature_probability {
let Some(entry) = self.weighted_entry(&entries) else {
return;
};
let count = entry.min
+ self
.world
.random
.next_int_bound((entry.max - entry.min + 1) as i32)
.unwrap_or(0) as u32;
let mut x = xo + self.world.random.next_int_bound(16).unwrap_or(0);
let mut z = zo + self.world.random.next_int_bound(16).unwrap_or(0);
let (sx, sz) = (x, z);
let Some(kind) = self.res.mob_types.get(&entry.entity).cloned() else {
continue;
};
for _ in 0..count {
let mut success = false;
let mut attempts = 0;
while !success && attempts < 4 {
attempts += 1;
let mut y = self.motion_top(x, z);
let placement = self.res.mob_data.placement(&entry.entity);
if placement == horizon_client::mobs::data::Placement::OnGround {
let below = self.world.block(x, y - 1, z);
if self.res.mob_data.state(below).flags & horizon_client::mobs::data::LAND
!= 0
{
y -= 1;
}
}
let ok = {
let w = self.mob_world_base(&[]);
spawning::placement_ok(kind.species, &w, x, y, z) && {
let mut r = self.world.random.clone();
spawning::spawn_rules_ok(kind.species, &w, &mut r, x, y, z)
}
};
if ok {
let width = kind.width;
let fx =
f64::from(x).clamp(f64::from(xo) + width, f64::from(xo) + 16.0 - width);
let fz =
f64::from(z).clamp(f64::from(zo) + width, f64::from(zo) + 16.0 - width);
if self
.spawn_mob(&entry.entity, [fx, f64::from(y), fz], true)
.is_some()
{
success = true;
}
}
x += self.world.random.next_int_bound(5).unwrap_or(0)
- self.world.random.next_int_bound(5).unwrap_or(0);
z += self.world.random.next_int_bound(5).unwrap_or(0)
- self.world.random.next_int_bound(5).unwrap_or(0);
while x < xo || x >= xo + 16 || z < zo || z >= zo + 16 {
x = sx + self.world.random.next_int_bound(5).unwrap_or(0)
- self.world.random.next_int_bound(5).unwrap_or(0);
z = sz + self.world.random.next_int_bound(5).unwrap_or(0)
- self.world.random.next_int_bound(5).unwrap_or(0);
}
}
}
}
}
pub(super) fn mob_world_base<'s>(&'s self, others: &'s [Seen]) -> MobWorld<'s> {
let (rain, thunder) = self.world.weather.levels(1.0);
MobWorld {
@@ -516,171 +412,6 @@ impl Game<'_> {
}
}
}
/// NaturalSpawner.spawnForChunk over the chunks within 8 of the player.
fn spawn_natural(&mut self) {
let Some(p) = self.player.as_ref().map(|p| p.position) else {
return;
};
let (pcx, pcz) = (p.x.floor() as i32 >> 4, p.z.floor() as i32 >> 4);
let mut counts = [0usize; 7];
for m in &self.mobs {
if let Some(i) = CATEGORIES.iter().position(|c| c.0 == m.kind.category) {
counts[i] += 1;
}
}
let creature_tick = self.world.game_time % 400 == 0;
let mut chunks: Vec<(i32, i32)> = (-8..=8)
.flat_map(|dx| (-8..=8).map(move |dz| (pcx + dx, pcz + dz)))
.filter(|c| self.world.chunks.contains_key(c))
.collect();
// ChunkMap: shuffled each tick.
for i in (1..chunks.len()).rev() {
let j = self.world.random.next_int_bound(i as i32 + 1).unwrap_or(0) as usize;
chunks.swap(i, j);
}
// ServerChunkCache.tickChunks: the chunks are shuffled every tick, but
// no category spawns without spawn_mobs, and only friendly ones
// unless the level is spawning monsters (ServerLevel.isSpawningMonsters).
let spawn_mobs = self.ui.rules.bool("spawn_mobs");
let spawn_enemies = spawn_mobs && self.ui.rules.bool("spawn_monsters");
for (cx, cz) in chunks {
for (ci, (category, cap)) in CATEGORIES.iter().enumerate() {
if !spawn_mobs
|| counts[ci] >= *cap
|| *category == "creature" && !creature_tick
|| *category == "monster" && !spawn_enemies
{
continue;
}
counts[ci] += self.spawn_category_for_chunk(category, cx, cz, p);
}
}
}
/// NaturalSpawner.spawnCategoryForPosition at a random point in the chunk.
fn spawn_category_for_chunk(
&mut self,
category: &str,
cx: i32,
cz: i32,
player: Vec3,
) -> usize {
let x0 = cx * 16 + self.world.random.next_int_bound(16).unwrap_or(0);
let z0 = cz * 16 + self.world.random.next_int_bound(16).unwrap_or(0);
let top = self.motion_top(x0, z0) + 1;
let min = horizon_client::chunk::MIN_Y;
let y = min + self.world.random.next_int_bound(top - min + 1).unwrap_or(0);
if y < min + 1 {
return 0;
}
let start = self.res.facts.get(self.world.block(x0, y, z0));
if start.full_collision {
return 0;
}
let mut spawned = 0;
for _ in 0..3 {
let (mut x, mut z) = (x0, z0);
let mut entry: Option<horizon_client::biome::SpawnEntry> = None;
let mut group = (self.world.random.next_float() * 4.0).ceil() as u32;
let mut cluster = 0;
let mut i = 0;
while i < group {
i += 1;
x += self.world.random.next_int_bound(6).unwrap_or(0)
- self.world.random.next_int_bound(6).unwrap_or(0);
z += self.world.random.next_int_bound(6).unwrap_or(0)
- self.world.random.next_int_bound(6).unwrap_or(0);
let (fx, fz) = (f64::from(x) + 0.5, f64::from(z) + 0.5);
let d2 = (fx - player.x).powi(2)
+ (f64::from(y) - player.y).powi(2)
+ (fz - player.z).powi(2);
if d2 <= 576.0 {
continue;
}
if entry.is_none() {
let Some(chunk) = self.world.chunks.get(&(x >> 4, z >> 4)) else {
break;
};
let biome = self.res.biomes.get(chunk.biome(x, y, z)).clone();
let list = match self.structure_spawns(category, [x, y, z]) {
Some(list) => list,
None => match biome.spawns.get(category).cloned() {
Some(list) => list,
None => break,
},
};
let list: Vec<_> = list
.into_iter()
.filter(|e| self.res.mob_types.contains_key(&e.entity))
.collect();
let Some(e) = self.weighted_entry(&list) else {
break;
};
group = e.min
+ self
.world
.random
.next_int_bound((e.max - e.min + 1) as i32)
.unwrap_or(0) as u32;
entry = Some(e);
}
let e = entry.clone().unwrap_or_else(|| unreachable!());
let despawn = if category == "water_ambient" {
64.0
} else {
128.0
};
if d2 > despawn * despawn {
continue;
}
let Some(kind) = self.res.mob_types.get(&e.entity).cloned() else {
break;
};
let ok = {
let w = self.mob_world_base(&[]);
let mut r = self.world.random.clone();
let placed = spawning::placement_ok(kind.species, &w, x, y, z)
&& spawning::spawn_rules_ok(kind.species, &w, &mut r, x, y, z);
let half = kind.width / 2.0;
let room = w.no_collision(Aabb::new(
Vec3::new(fx - half, f64::from(y), fz - half),
Vec3::new(fx + half, f64::from(y) + kind.height, fz + half),
));
// PathfinderMob.checkSpawnRules: walk target value >= 0 (monsters need darkness).
let light_ok = !kind.category.eq("monster")
|| matches!(
kind.species,
Species::Slime
| Species::SulfurCube
| Species::Ghast
| Species::MagmaCube
| Species::Guardian
| Species::ElderGuardian
)
|| w.brightness(x, y, z) <= 0.5;
let consumed = r;
(placed && room && light_ok, consumed)
};
self.world.random = ok.1;
if !ok.0 {
continue;
}
if d2 > 16384.0 && kind.category != "creature" {
continue;
}
if self
.spawn_mob(&e.entity, [fx, f64::from(y), fz], false)
.is_some()
{
cluster += 1;
spawned += 1;
if spawned >= 4 || cluster >= group.max(1) {
return spawned;
}
}
}
}
spawned
}
/// 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);
@@ -0,0 +1,355 @@
//! The game's side of `NaturalSpawner` (`mobs::spawn_selection`): the level
//! 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::{
biome::{BiomeInfo, SpawnEntry},
blocks::BlockFacts,
collisions::{SceneCollisions, no_collision_empty},
mobs::{
Mob, MobWorld, Species,
data::{self, MobData},
spawn_rules::{Dimension, SpawnLevel},
spawn_selection::{
self, Category, Counted, Heightmap, NaturalLevel, SpawnCost, SpawnPlayer, SpawnRequest,
SpawnState, Spawned, StructureAt, StructureSpawns,
},
},
};
use horizon_core::{
geometry::{Aabb, Vec3},
random::{LegacyRandom, RandomSource, WorldgenRandom},
};
use horizon_world::registry::BlockStateId;
use std::collections::HashMap;
/// The game level as natural spawning reads it.
struct GameLevel<'g, 'a> {
game: &'g Game<'a>,
w: MobWorld<'g>,
/// Boxes of the mobs this pass created, not yet in the game's list.
pending: std::cell::RefCell<Vec<Aabb>>,
}
impl GameLevel<'_, '_> {
fn biome(&self, x: i32, y: i32, z: i32) -> Option<&BiomeInfo> {
let chunk = self.game.world.chunks.get(&(x >> 4, z >> 4))?;
Some(self.game.res.biomes.get(chunk.biome(x, y, z)))
}
}
impl SpawnLevel for GameLevel<'_, '_> {
fn state(&self, x: i32, y: i32, z: i32) -> BlockStateId {
self.w.state(x, y, z)
}
fn facts(&self) -> &BlockFacts {
self.w.facts
}
fn mob_data(&self) -> &MobData {
self.w.data
}
fn block_tag(&self, tag: &str, block: &str) -> bool {
self.w.block_tag(tag, block)
}
fn biome_tag(&self, tag: &str, x: i32, y: i32, z: i32) -> bool {
self.w.biome_tag(tag, x, y, z)
}
fn sky_light(&self, x: i32, y: i32, z: i32) -> i32 {
self.w.sky_light(x, y, z)
}
fn block_light(&self, x: i32, y: i32, z: i32) -> i32 {
self.w.block_light(x, y, z)
}
fn sky_darken(&self) -> i32 {
SpawnLevel::sky_darken(&self.w)
}
fn thundering(&self) -> bool {
self.w.thundering
}
fn sea_level(&self) -> i32 {
self.w.sea_level()
}
fn world_surface(&self, x: i32, z: i32) -> i32 {
self.w.world_surface(x, z)
}
fn peaceful(&self) -> bool {
self.w.peaceful()
}
fn dimension(&self) -> Dimension {
self.w.dimension()
}
fn surface_slime_chance(&self, x: i32, y: i32, z: i32) -> f32 {
self.w.surface_slime_chance(x, y, z)
}
fn world_seed(&self) -> i64 {
self.w.world_seed
}
fn player_near(&self, x: f64, y: f64, z: f64, distance: f64) -> bool {
self.w.player_near(x, y, z, distance)
}
}
impl NaturalLevel for GameLevel<'_, '_> {
fn min_y(&self) -> i32 {
horizon_client::chunk::MIN_Y
}
fn max_y(&self) -> i32 {
horizon_client::chunk::MIN_Y + horizon_client::chunk::HEIGHT - 1
}
fn ambient_light(&self) -> f32 {
match self.game.dim {
Dim::Nether => 0.1,
Dim::End => 0.25,
_ => 0.0,
}
}
fn has_ceiling(&self) -> bool {
self.game.dim == Dim::Nether
}
fn mob_type(&self, entity: &str) -> Option<(Species, f32, f32, Category)> {
let t = self.game.res.mob_types.get(entity)?;
Some((
t.species,
t.width as f32,
t.height as f32,
Category::from_name(&t.category),
))
}
fn biome_spawns(&self, x: i32, y: i32, z: i32) -> Option<&HashMap<String, Vec<SpawnEntry>>> {
self.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()
}
fn creature_probability(&self, x: i32, y: i32, z: i32) -> f32 {
self.biome(x, y, z).map_or(0.1, |b| b.creature_probability)
}
fn structures_at(&self, x: i32, y: i32, z: i32) -> Vec<StructureAt> {
self.game
.structures_reaching([x, y, z])
.into_iter()
.map(|(id, in_piece)| StructureAt {
id,
in_piece,
in_bounds: true,
})
.collect()
}
fn structure_spawns(&self) -> &StructureSpawns {
StructureSpawns::shared(&self.game.options.resources)
}
fn nearest_player_distance_sqr(&self, x: f64, y: f64, z: f64) -> Option<f64> {
let p = self.game.player.as_ref()?;
if self.game.spectator() {
return None;
}
let d = [p.position.x - x, p.position.y - y, p.position.z - z];
Some(d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
}
fn respawn(&self) -> Option<[i32; 3]> {
// The world spawn point lives in the overworld.
if self.game.dim != Dim::Overworld {
return None;
}
self.game.level.spawn.map(|s| {
[
s[0].floor() as i32,
s[1].floor() as i32,
s[2].floor() as i32,
]
})
}
fn can_spawn_in_chunk(&self, cx: i32, cz: i32) -> bool {
self.game.world.chunks.contains_key(&(cx, cz))
}
fn no_collision(&self, bounds: Aabb) -> bool {
let mut scene = SceneCollisions {
world: &self.game.world,
binding: self.w.collision,
};
no_collision_empty(&mut scene, bounds)
}
fn unobstructed(&self, bounds: Aabb) -> bool {
let player = self
.game
.player_view()
.is_some_and(|p| p.alive && !p.spectator && p.bounds.intersects(bounds));
!player
&& !self.pending.borrow().iter().any(|b| b.intersects(bounds))
&& !self
.game
.mobs
.iter()
.any(|m| m.alive() && !m.removed && m.bounds().intersects(bounds))
}
fn spawn_mobs(&self) -> bool {
self.game.ui.rules.bool("spawn_mobs")
}
fn heightmap(&self, kind: Heightmap, x: i32, z: i32) -> i32 {
let top = horizon_client::chunk::MIN_Y + horizon_client::chunk::HEIGHT;
(horizon_client::chunk::MIN_Y..top)
.rev()
.find(|&y| spawn_selection::heightmap_blocks(self, kind, x, y, z))
.map_or(horizon_client::chunk::MIN_Y, |y| y + 1)
}
fn pathfindable_land(&self, x: i32, y: i32, z: i32) -> bool {
self.w.data.state(self.state(x, y, z)).flags & data::LAND != 0
}
}
impl<'a> Game<'a> {
fn natural_level(&self) -> GameLevel<'_, 'a> {
GameLevel {
game: self,
w: self.mob_world_base(&[]),
pending: Default::default(),
}
}
/// Creates a mob the spawner accepted, finalized the game's way; the
/// caller adds it with [`Game::add_spawned`].
fn finalize_spawned(&self, r: &SpawnRequest, pending: &mut Vec<Mob>) -> Option<Spawned> {
let kind = self.res.mob_types.get(r.entity)?.clone();
let [x, y, z] = r.position;
let mut mob = Mob::new(0, kind, Vec3::new(x, y, z), r.yaw);
let facts = self.biome_facts(x.floor() as i32, y.floor() as i32, z.floor() as i32);
let difficulty = self.local_difficulty(r.position);
horizon_client::mobs::spawning::finalize(&mut mob, &facts, &difficulty);
horizon_client::mobs::raider::finalize(
&mut mob,
horizon_client::mobs::raider::Reason::Natural,
);
let bounds = mob.bounds();
pending.push(mob);
Some(Spawned {
bounds,
school: true,
})
}
/// Adds the mobs a spawn pass created: ids, enchanted gear and jockeys.
fn add_spawned(&mut self, pending: Vec<Mob>, persistent: bool) {
for mut mob in pending {
self.next_entity += 1;
mob.id = self.next_entity;
mob.persistent = persistent;
let id = mob.id;
self.mobs.push(mob);
self.enchant_spawn_equipment(id);
if !persistent {
self.spawn_jockeys(id, true);
}
}
}
/// `ServerChunkCache.tickChunks`: the spawn state over the loaded mobs,
/// the categories under their global caps (creatures every 400 ticks),
/// then `spawnForChunk` in the shuffled chunks within 128 blocks of the
/// player.
pub(super) fn spawn_natural(&mut self) {
let Some(p) = self.player.as_ref().map(|p| p.position) else {
return;
};
let (pcx, pcz) = (p.x.floor() as i32 >> 4, p.z.floor() as i32 >> 4);
// ChunkMap.collectSpawningChunks: tracked chunks whose centre is
// within 128 blocks of a non-spectator.
let spectator = self.spectator();
let mut chunks: Vec<(i32, i32)> = (-8..=8)
.flat_map(|dx| (-8..=8).map(move |dz| (pcx + dx, pcz + dz)))
.filter(|&(cx, cz)| {
let (x, z) = (f64::from(cx * 16 + 8) - p.x, f64::from(cz * 16 + 8) - p.z);
!spectator && x * x + z * z < 16384.0 && self.world.chunks.contains_key(&(cx, cz))
})
.collect();
let spawn_mobs = self.ui.rules.bool("spawn_mobs");
let spawn_enemies = self.ui.rules.bool("spawn_monsters");
let mut random = std::mem::replace(&mut self.world.random, LegacyRandom::new(0));
let mut pending = Vec::new();
{
let level = self.natural_level();
let counted: Vec<Counted> = self
.mobs
.iter()
.map(|m| Counted {
entity: &m.kind.id,
category: Category::from_name(&m.kind.category),
block: [
m.position.x.floor() as i32,
m.position.y.floor() as i32,
m.position.z.floor() as i32,
],
is_mob: true,
persistent: m.persistent,
})
.collect();
let players = vec![SpawnPlayer {
position: [p.x, p.y, p.z],
spectator,
}];
// A single player's natural spawn chunk tracker covers 17 x 17 chunks.
let mut state = SpawnState::new(&level, 289, players, &counted, |cx, cz| {
self.world.chunks.contains_key(&(cx, cz))
});
let categories = if spawn_mobs {
state.filtered_categories(spawn_enemies, self.world.game_time % 400 == 0)
} else {
Vec::new()
};
// Util.shuffle.
for i in (1..chunks.len()).rev() {
let j = random.next_int_bound(i as i32 + 1).unwrap_or(0) as usize;
chunks.swap(i, j);
}
if !categories.is_empty() {
for chunk in chunks {
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);
}
spawned
};
spawn_selection::spawn_for_chunk(
&level,
&mut random,
&mut state,
&categories,
chunk,
&mut spawn,
&mut |_| {},
);
}
}
}
self.world.random = random;
self.add_spawned(pending, false);
}
/// `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
/// 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 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);
}
spawned
};
spawn_selection::spawn_mobs_for_chunk_generation(
&level,
source,
(cx, cz),
&mut random,
&mut rules,
&mut spawn,
&mut |_| {},
);
}
self.add_spawned(pending, true);
}
}
+1 -80
View File
@@ -6,7 +6,7 @@ use super::Game;
impl Game<'_> {
/// Structures reaching `pos`: (id, inside a piece). `getStructureAt`
/// tests the start's whole box, `getStructureWithPieceAt` a piece's.
fn structures_reaching(&self, pos: [i32; 3]) -> Vec<(String, bool)> {
pub(super) fn structures_reaching(&self, pos: [i32; 3]) -> Vec<(String, bool)> {
let Some(chunk) = self.world.chunks.get(&(pos[0] >> 4, pos[2] >> 4)) else {
return Vec::new();
};
@@ -39,83 +39,4 @@ impl Game<'_> {
.map(|(n, _)| n)
.collect()
}
/// `ChunkGenerator.getMobsAt` / `NaturalSpawner.mobsAt`: a structure's
/// `spawn_overrides` for the category replace the biome's list where its
/// box (or piece) holds `pos`; fortress spawns on nether bricks.
pub(super) fn structure_spawns(
&mut self,
category: &str,
pos: [i32; 3],
) -> Option<Vec<horizon_client::biome::SpawnEntry>> {
let reaching = self.structures_reaching(pos);
if reaching.is_empty() {
return None;
}
let below = self.world.block(pos[0], pos[1] - 1, pos[2]);
let bricks = &*self.res.facts.get(below).name == "minecraft:nether_bricks";
for (id, piece) in &reaching {
let Some(o) = self.spawn_override(id, category) else {
continue;
};
if !o.0 || *piece {
return Some(o.1);
}
}
if category == "monster"
&& bricks
&& reaching.iter().any(|(n, _)| n == "minecraft:fortress")
{
return self
.spawn_override("minecraft:fortress", "monster")
.map(|o| o.1);
}
None
}
/// A structure's override for a category: (piece-only, spawns).
fn spawn_override(
&mut self,
structure: &str,
category: &str,
) -> Option<(bool, Vec<horizon_client::biome::SpawnEntry>)> {
if !self.spawn_overrides.contains_key(structure) {
let (ns, path) = structure
.split_once(':')
.unwrap_or(("minecraft", structure));
let file = self
.options
.resources
.join(format!("data/{ns}/worldgen/structure/{path}.json"));
let v: serde_json::Value = std::fs::read(file)
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default();
let mut map = std::collections::HashMap::new();
for (cat, o) in v["spawn_overrides"].as_object().into_iter().flatten() {
let piece = o["bounding_box"].as_str() == Some("piece");
let spawns = o["spawns"]
.as_array()
.into_iter()
.flatten()
.map(|e| {
let count = &e["count"];
horizon_client::biome::SpawnEntry {
entity: e["type"].as_str().unwrap_or_default().to_owned(),
weight: e["weight"].as_u64().unwrap_or(1) as u32,
min: count["min_inclusive"]
.as_u64()
.or_else(|| e["minCount"].as_u64())
.unwrap_or(1) as u32,
max: count["max_inclusive"]
.as_u64()
.or_else(|| e["maxCount"].as_u64())
.unwrap_or(1) as u32,
}
})
.collect();
map.insert(cat.clone(), (piece, spawns));
}
self.spawn_overrides.insert(structure.to_owned(), map);
}
self.spawn_overrides.get(structure)?.get(category).cloned()
}
}
+1
View File
@@ -47,6 +47,7 @@ pub mod riders;
pub mod shulker;
pub mod sniffer;
pub mod spawn_rules;
pub mod spawn_selection;
pub mod spawning;
pub mod species;
pub mod sulfur_cube;
+16
View File
@@ -43,6 +43,8 @@ pub enum Placement {
pub struct MobData {
pub states: Vec<StateData>,
pub placements: HashMap<String, Placement>,
/// Each entity type's `SpawnPlacements.getHeightmapType`.
pub heightmaps: HashMap<String, String>,
pub animations: HashMap<String, Animation>,
/// Biome tags (spawns_warm_variant_farm_animals, is_ocean...) by tag id.
pub biome_tags: HashMap<String, std::collections::HashSet<String>>,
@@ -109,6 +111,12 @@ impl MobData {
(k.clone(), placement)
})
.collect();
let heightmaps = v["placements"]
.as_object()
.into_iter()
.flatten()
.filter_map(|(k, p)| Some((k.clone(), p["heightmap"].as_str()?.to_owned())))
.collect();
let animations = v["animations"]
.as_object()
.into_iter()
@@ -118,6 +126,7 @@ impl MobData {
Self {
states,
placements,
heightmaps,
animations,
biome_tags: HashMap::new(),
entity_tags: HashMap::new(),
@@ -171,6 +180,7 @@ impl MobData {
Self {
states,
placements: HashMap::new(),
heightmaps: HashMap::new(),
animations: HashMap::new(),
biome_tags: HashMap::new(),
entity_tags: HashMap::new(),
@@ -186,6 +196,12 @@ impl MobData {
top: 1.0,
})
}
/// `SpawnPlacements.getHeightmapType` (MOTION_BLOCKING_NO_LEAVES unless registered).
pub fn heightmap(&self, entity: &str) -> &str {
self.heightmaps
.get(entity)
.map_or("MOTION_BLOCKING_NO_LEAVES", String::as_str)
}
pub fn placement(&self, entity: &str) -> Placement {
self.placements
.get(entity)
File diff suppressed because it is too large Load Diff
+27 -1
View File
@@ -82,7 +82,10 @@ impl Connection {
}
/// Connection.setEncryptionKey: both directions keyed with the shared secret.
pub fn enable_encryption(&mut self, secret: &[u8; 16]) {
self.reader.decrypt = Some(Cfb8::new(secret, secret));
let mut decrypt = Cfb8::new(secret, secret);
// Bytes already read past the last packet were sent encrypted.
decrypt.decrypt(&mut self.reader.buffer);
self.reader.decrypt = Some(decrypt);
self.writer.encrypt = Some(Cfb8::new(secret, secret));
}
pub fn send(&mut self, body: &[u8]) -> Result<()> {
@@ -180,4 +183,27 @@ mod tests {
assert_eq!((id, payload.as_slice()), (9, &b"reply"[..]));
server.join().unwrap();
}
/// Encrypted bytes that arrive in the same read as the last plain packet
/// are decrypted once encryption is enabled.
#[test]
fn encrypted_bytes_read_before_enabling_encryption() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let (sent, both_sent) = std::sync::mpsc::channel();
let client = std::thread::spawn(move || {
let mut c = Connection::connect("127.0.0.1", port, Duration::from_secs(5)).unwrap();
c.send(&[0, 1, 2]).unwrap();
c.enable_encryption(&[7; 16]);
c.send(&[5, 4, 3]).unwrap();
sent.send(c).unwrap();
});
let (stream, _) = listener.accept().unwrap();
let mut c = Connection::from_stream(stream).unwrap();
let _keep_open = both_sent.recv().unwrap();
assert_eq!(c.recv().unwrap(), (0, vec![1, 2]));
c.enable_encryption(&[7; 16]);
assert_eq!(c.recv().unwrap(), (5, vec![4, 3]));
client.join().unwrap();
}
}
@@ -0,0 +1,550 @@
package dev.horizonlab.parity;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import java.io.PrintWriter;
import java.lang.reflect.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import net.minecraft.SharedConstants;
import net.minecraft.core.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.RegistryDataLoader;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.RegistryLayer;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.repository.ServerPacksSource;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.tags.TagLoader;
import net.minecraft.util.RandomSource;
import net.minecraft.world.Difficulty;
import net.minecraft.world.DifficultyInstance;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.*;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.entity.EntityTypeTest;
import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.util.Mth;
import net.minecraft.world.level.levelgen.structure.BoundingBox;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.StructurePiece;
import net.minecraft.world.level.levelgen.structure.StructureStart;
import net.minecraft.world.level.levelgen.structure.pieces.PiecesContainer;
import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/**
* Original oracle: natural spawning as NaturalSpawner decides it, on a
* stand-in ServerLevel built from boxes of blocks, light and biomes, with
* structure starts (pieces), existing entities, players, the world spawn
* point and unloaded chunks. Three modes:
*
* - chunk: createState over the entities, getFilteredSpawningCategories, then
* spawnForChunk's loop (canSpawnForCategoryLocal, getRandomPosWithin) and
* vanilla's spawnCategoryForPosition with the state's canSpawn and
* afterSpawn;
* - pos: spawnCategoryForPosition from a given start;
* - gen: spawnMobsForChunkGeneration.
*
* The log records each attempt (the nearest player lookup), each type that
* passed isValidSpawnPostitionForType with the potential test's answer, each
* spawn (position and yaw bits), and the next draw of the level random, which
* pins every draw in between. finalizeSpawn reads level.getRandom(), which
* here is a fixed "neutral" source (nextInt 0, nextFloat and nextDouble 0.75),
* so finalizeSpawn's own outcomes stay out of scope while its effect on later
* attempts (the finalized mob's box obstructs them) is deterministic.
*/
public final class VanillaNaturalSpawnOracle {
static Object unsafe;
static Method allocate, putObject, objectFieldOffset;
static Object alloc(Class<?> c) throws Exception { return allocate.invoke(unsafe, c); }
static void put(Object target, Class<?> owner, String field, Object value) throws Exception {
Field f = owner.getDeclaredField(field);
putObject.invoke(unsafe, target, (long) objectFieldOffset.invoke(unsafe, f), value);
}
record Box(int x0, int y0, int z0, int x1, int y1, int z1) {
boolean has(int x, int y, int z) { return x >= x0 && x <= x1 && y >= y0 && y <= y1 && z >= z0 && z <= z1; }
static Box parse(String s) {
String[] p = s.split(",");
return new Box(Integer.parseInt(p[0]), Integer.parseInt(p[1]), Integer.parseInt(p[2]),
Integer.parseInt(p[3]), Integer.parseInt(p[4]), Integer.parseInt(p[5]));
}
}
record Start(Structure structure, StructureStart start) {}
/** finalizeSpawn's random: the same answer every time. */
static final class Neutral implements RandomSource {
@Override public RandomSource fork() { return this; }
@Override public net.minecraft.world.level.levelgen.PositionalRandomFactory forkPositional() { throw new UnsupportedOperationException(); }
@Override public void setSeed(long seed) {}
@Override public int nextInt() { return 0; }
@Override public int nextInt(int bound) { return 0; }
@Override public long nextLong() { return 0L; }
@Override public boolean nextBoolean() { return false; }
@Override public float nextFloat() { return 0.75F; }
@Override public double nextDouble() { return 0.75; }
@Override public double nextGaussian() { return 0.0; }
}
static final class Lvl extends ServerLevel {
Lvl() { super(null, null, null, null, null, null, false, 0L, List.of(), false); }
RegistryAccess registries;
net.minecraft.server.MinecraftServer fake;
@Override public net.minecraft.server.MinecraftServer getServer() { return fake; }
List<Map.Entry<Box, BlockState>> blocks = new ArrayList<>();
List<Map.Entry<Box, int[]>> lights = new ArrayList<>();
List<Map.Entry<Box, Holder<Biome>>> biomes = new ArrayList<>();
Holder<Biome> biome;
int sky, block, darken, sea;
boolean thunder;
long seed;
Difficulty difficulty;
DimensionType dimensionType;
ResourceKey<Level> dimensionKey;
LevelData.RespawnData respawn;
Set<Long> unloaded = new HashSet<>();
List<Start> starts = new ArrayList<>();
List<Entity> entities = new ArrayList<>();
List<ServerPlayer> playerList = new ArrayList<>();
net.minecraft.world.attribute.EnvironmentAttributeSystem attributes;
BiomeManager biomeManager;
WorldBorder border = new WorldBorder();
GameRules rules;
ServerChunkCache chunkSource;
StructureManager structures;
RandomSource neutral = new Neutral();
List<String> log;
boolean logColumns;
int next = 1;
@Override public RegistryAccess registryAccess() { return registries; }
@Override public Difficulty getDifficulty() { return difficulty; }
@Override public long getGameTime() { return 0L; }
@Override public int getNextEntityId() { return next++; }
@Override public net.minecraft.world.flag.FeatureFlagSet enabledFeatures() { return net.minecraft.world.flag.FeatureFlags.REGISTRY.allFlags(); }
@Override public BlockState getBlockState(BlockPos pos) {
for (int i = blocks.size() - 1; i >= 0; i--)
if (blocks.get(i).getKey().has(pos.getX(), pos.getY(), pos.getZ())) return blocks.get(i).getValue();
return Blocks.AIR.defaultBlockState();
}
@Override public FluidState getFluidState(BlockPos pos) { return getBlockState(pos).getFluidState(); }
int[] light(BlockPos pos) {
for (int i = lights.size() - 1; i >= 0; i--)
if (lights.get(i).getKey().has(pos.getX(), pos.getY(), pos.getZ())) return lights.get(i).getValue();
return new int[] {sky, block};
}
@Override public int getBrightness(LightLayer layer, BlockPos pos) { int[] l = light(pos); return layer == LightLayer.SKY ? l[0] : l[1]; }
@Override public int getRawBrightness(BlockPos pos, int darkening) { int[] l = light(pos); return Math.max(l[1], l[0] - darkening); }
@Override public int getSkyDarken() { return darken; }
@Override public boolean isThundering() { return thunder; }
@Override public int getSeaLevel() { return sea; }
@Override public int getHeight(Heightmap.Types type, int x, int z) {
if (logColumns) log.add("col " + x + " " + z);
for (int y = getMaxY(); y >= getMinY(); y--)
if (type.isOpaque().test(getBlockState(new BlockPos(x, y, z)))) return y + 1;
return getMinY();
}
@Override public long getSeed() { return seed; }
@Override public DimensionType dimensionType() { return dimensionType; }
@Override public ResourceKey<Level> dimension() { return dimensionKey; }
Holder<Biome> biomeAt(int x, int y, int z) {
for (int i = biomes.size() - 1; i >= 0; i--)
if (biomes.get(i).getKey().has(x, y, z)) return biomes.get(i).getValue();
return biome;
}
@Override public Holder<Biome> getBiome(BlockPos pos) { return biomeAt(pos.getX(), pos.getY(), pos.getZ()); }
@Override public BiomeManager getBiomeManager() { return biomeManager; }
@Override public net.minecraft.world.attribute.EnvironmentAttributeSystem environmentAttributes() { return attributes; }
@Override public WorldBorder getWorldBorder() { return border; }
@Override public GameRules getGameRules() { return rules; }
@Override public RandomSource getRandom() { return neutral; }
@Override public ServerChunkCache getChunkSource() { return chunkSource; }
@Override public StructureManager structureManager() { return structures; }
@Override public LevelData.RespawnData getRespawnData() { return respawn; }
@Override public boolean canSpawnEntitiesInChunk(ChunkPos pos) { return !unloaded.contains(pos.pack()); }
@Override public DifficultyInstance getCurrentDifficultyAt(BlockPos pos) { return new DifficultyInstance(difficulty, 0L, 0L, 0.0F); }
@Override public BlockGetter getChunkForCollisions(int chunkX, int chunkZ) { return this; }
@Override public boolean containsAnyLiquid(AABB box) {
BlockPos lo = BlockPos.containing(box.minX, box.minY, box.minZ), hi = BlockPos.containing(box.maxX, box.maxY, box.maxZ);
for (BlockPos p : BlockPos.betweenClosed(lo, hi)) if (!getBlockState(p).getFluidState().isEmpty()) return true;
return false;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override public List players() { return playerList; }
@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<>();
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<>();
for (Entity e : entities) {
T t = type.tryCast(e);
if (t != null && e.getBoundingBox().intersects(bb) && selector.test(t)) out.add(t);
}
return out;
}
@Override public boolean addFreshEntity(Entity entity) { entities.add(entity); return true; }
@Override public void addFreshEntityWithPassengers(Entity entity) { entity.getSelfAndPassengers().forEach(this::addFreshEntity); }
@Override public Player getNearestPlayer(double x, double y, double z, double maxDist, boolean filter) {
if (maxDist == -1.0 && !filter) log.add("try " + (int) Math.floor(x) + " " + (int) Math.floor(z));
return super.getNearestPlayer(x, y, z, maxDist, filter);
}
}
static final class Chunk extends LevelChunk {
Chunk() { super((Level) null, (ChunkPos) null); }
Lvl level;
ChunkPos at;
@Override public ChunkPos getPos() { return at; }
@Override public BlockState getBlockState(BlockPos pos) { return level.getBlockState(pos); }
@Override public FluidState getFluidState(BlockPos pos) { return level.getFluidState(pos); }
@Override public int getHeight(Heightmap.Types type, int x, int z) { return level.getHeight(type, x, z); }
}
static final class Source extends ServerChunkCache {
Source() { super(null, null, null, null, null, null, 0, 0, false, null); }
ChunkGenerator generator;
@Override public ChunkGenerator getGenerator() { return generator; }
}
/** getPlayersCloseForSpawning: none past the natural spawn chunk tracker's 8 chunks, else non-spectators within 128 blocks of the chunk centre. */
static final class Map2 extends ChunkMap {
Map2() { super(null, null, null, null, null, null, null, null, null, null, 0, false); }
Lvl level;
@Override public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) {
boolean tracked = false;
for (ServerPlayer p : level.playerList) {
int px = Mth.floor(p.getX()) >> 4, pz = Mth.floor(p.getZ()) >> 4;
if (Math.max(Math.abs(px - pos.x()), Math.abs(pz - pos.z())) <= 8) tracked = true;
}
if (!tracked) return List.of();
List<ServerPlayer> out = new ArrayList<>();
for (ServerPlayer p : level.playerList) {
double xd = pos.x() * 16 + 8 - p.getX(), zd = pos.z() * 16 + 8 - p.getZ();
if (!p.isSpectator() && xd * xd + zd * zd < 16384.0) out.add(p);
}
return out;
}
}
static final class Structures extends StructureManager {
Structures() { super(null, null, null); }
Lvl level;
List<Start> referenced(int x, int z) {
int cx = x >> 4, cz = z >> 4;
List<Start> out = new ArrayList<>();
for (Start s : level.starts) {
BoundingBox b = s.start().getBoundingBox();
if (b.intersects(cx * 16, cz * 16, cx * 16 + 15, cz * 16 + 15)) out.add(s);
}
return out;
}
@Override public Map<Structure, LongSet> getAllStructuresAt(int x, int y, int z) {
Map<Structure, LongSet> out = new LinkedHashMap<>();
for (Start s : referenced(x, z)) out.computeIfAbsent(s.structure(), k -> new LongOpenHashSet()).add(level.starts.indexOf(s));
return out;
}
@Override public void fillStartsForStructure(Structure structure, LongSet references, Consumer<StructureStart> consumer) {
for (long i : references) {
StructureStart start = level.starts.get((int) i).start();
if (start.isValid()) consumer.accept(start);
}
}
@Override public List<StructureStart> startsForStructure(int sectionX, int sectionZ, Structure structure) {
List<StructureStart> out = new ArrayList<>();
for (Start s : referenced(sectionX << 4, sectionZ << 4)) if (s.structure() == structure) out.add(s.start());
return out;
}
@Override public RegistryAccess registryAccess() { return level.registries; }
}
static final class Piece extends StructurePiece {
Piece(BoundingBox box) { super(null, 0, box); }
@Override protected void addAdditionalSaveData(StructurePieceSerializationContext context, CompoundTag tag) {}
@Override public void postProcess(WorldGenLevel level, StructureManager structureManager, ChunkGenerator generator, RandomSource random,
BoundingBox chunkBB, ChunkPos chunkPos, BlockPos referencePos) {}
}
static final class OraclePlayer extends ServerPlayer {
OraclePlayer() { super(null, null, null, null); }
GameType mode;
@Override public boolean isSpectator() { return mode == GameType.SPECTATOR; }
@Override public boolean isCreative() { return mode == GameType.CREATIVE; }
@Override public GameType gameMode() { return mode; }
}
static String bits(double v) { return Long.toHexString(Double.doubleToRawLongBits(v)); }
/**
* A spawned mob's yaw. A cube mob whose size finalizeSpawn changed took its
* constructor's head rotation (AbstractCubeMob.onSyncedDataUpdated), drawn
* from the entity's own unseeded random: not reproducible, so not compared.
*/
static String yaw(Entity e) {
if (e instanceof net.minecraft.world.entity.monster.cubemob.AbstractCubeMob cube && cube.getSize() != 1) return "head";
return bits(e.getYRot());
}
static String bits(float v) { return Integer.toHexString(Float.floatToRawIntBits(v)); }
public static void main(String[] args) throws Exception {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
var unsafeField = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = unsafeField.get(null);
allocate = unsafe.getClass().getMethod("allocateInstance", Class.class);
putObject = unsafe.getClass().getMethod("putObject", Object.class, long.class, Object.class);
objectFieldOffset = unsafe.getClass().getMethod("objectFieldOffset", Field.class);
var repo = ServerPacksSource.createVanillaTrustedRepository();
repo.reload();
repo.setSelected(List.of("vanilla"));
var resources = new MultiPackResourceManager(PackType.SERVER_DATA, repo.openAllSelected());
var layers = RegistryLayer.createRegistryAccess();
var staticTags = TagLoader.loadTagsForExistingRegistries(resources, layers.getLayer(RegistryLayer.STATIC));
var context = TagLoader.buildUpdatedLookups(layers.getAccessForLoading(RegistryLayer.WORLD), staticTags);
RegistryAccess.Frozen world = RegistryDataLoader.load(resources, context, RegistryDataLoader.WORLD_REGISTRIES, Runnable::run).join();
var worldLayers = layers.replaceFrom(RegistryLayer.WORLD, world);
var loaded = net.minecraft.server.ReloadableServerRegistries.reload(worldLayers, staticTags, resources, Runnable::run).join();
HolderLookup.Provider lookup = loaded.lookupWithUpdatedTags();
var components = BuiltInRegistries.DATA_COMPONENT_INITIALIZERS.build(lookup);
staticTags.forEach(Registry.PendingTags::apply);
components.forEach(net.minecraft.core.component.DataComponentInitializers.PendingComponents::apply);
RegistryAccess access = loaded.layers().compositeAccess();
var biomes = access.lookupOrThrow(Registries.BIOME);
var dimensions = access.lookupOrThrow(Registries.DIMENSION_TYPE);
var blockLookup = access.lookupOrThrow(Registries.BLOCK);
var structureLookup = access.lookupOrThrow(Registries.STRUCTURE);
// A server that only answers debug subscribers (path navigation asks).
var server = (net.minecraft.server.MinecraftServer) alloc(net.minecraft.server.dedicated.DedicatedServer.class);
put(server, net.minecraft.server.MinecraftServer.class, "debugSubscribers", alloc(net.minecraft.util.debug.ServerDebugSubscribers.class));
Lvl level = (Lvl) alloc(Lvl.class);
level.fake = server;
put(level, ServerLevel.class, "server", server);
level.registries = access;
level.blocks = new ArrayList<>();
level.lights = new ArrayList<>();
level.biomes = new ArrayList<>();
level.unloaded = new HashSet<>();
level.starts = new ArrayList<>();
level.entities = new ArrayList<>();
level.playerList = new ArrayList<>();
level.border = new WorldBorder();
level.neutral = new Neutral();
level.rules = new GameRules(net.minecraft.world.flag.FeatureFlags.REGISTRY.allFlags());
level.biomeManager = new BiomeManager((x, y, z) -> level.biome, 0L) {
@Override public Holder<Biome> getBiome(BlockPos pos) { return level.biomeAt(pos.getX(), pos.getY(), pos.getZ()); }
@Override public Holder<Biome> getBiome(int x, int y, int z) { return level.biomeAt(x, y, z); }
};
Source source = (Source) alloc(Source.class);
source.generator = (ChunkGenerator) alloc(FlatLevelSource.class);
level.chunkSource = source;
Structures structures = (Structures) alloc(Structures.class);
structures.level = level;
level.structures = structures;
Map2 chunkMap = (Map2) alloc(Map2.class);
chunkMap.level = level;
Method createState = NaturalSpawner.class.getMethod("createState", int.class, ServerLevel.class, NaturalSpawner.ChunkGetter.class, LocalMobCapCalculator.class);
Method canSpawn = NaturalSpawner.SpawnState.class.getDeclaredMethod("canSpawn", EntityType.class, Level.class, BlockPos.class, net.minecraft.world.level.chunk.ChunkAccess.class);
canSpawn.setAccessible(true);
Method afterSpawn = NaturalSpawner.SpawnState.class.getDeclaredMethod("afterSpawn", Mob.class, net.minecraft.world.level.chunk.ChunkAccess.class);
afterSpawn.setAccessible(true);
Method local = NaturalSpawner.SpawnState.class.getDeclaredMethod("canSpawnForCategoryLocal", MobCategory.class, ChunkPos.class);
local.setAccessible(true);
Method randomPos = NaturalSpawner.class.getDeclaredMethod("getRandomPosWithin", Level.class, LevelChunk.class);
randomPos.setAccessible(true);
Field entityId = Entity.class.getDeclaredField("id");
entityId.setAccessible(true);
List<String> lines = Files.readAllLines(Path.of(args[0]));
try (var w = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8))) {
for (String raw : lines) {
if (raw.isBlank()) continue;
Map<String, String> c = new LinkedHashMap<>();
for (String token : raw.trim().split("\\s+")) {
int eq = token.indexOf('=');
if (eq < 0) c.put("name", token); else c.put(token.substring(0, eq), token.substring(eq + 1));
}
List<String> log = new ArrayList<>();
level.log = log;
level.logColumns = false;
try {
level.dimensionKey = ResourceKey.create(Registries.DIMENSION, Identifier.parse(c.getOrDefault("dim", "minecraft:overworld")));
level.dimensionType = dimensions.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, level.dimensionKey.identifier())).value();
level.blocks.clear();
for (int i = 0; c.containsKey("k" + i); i++) {
String[] p = c.get("k" + i).split("/", 2);
level.blocks.add(Map.entry(Box.parse(p[0]), net.minecraft.commands.arguments.blocks.BlockStateParser.parseForBlock(blockLookup, p[1], false).blockState()));
}
level.lights.clear();
for (int i = 0; c.containsKey("l" + i); i++) {
String[] p = c.get("l" + i).split("/", 2);
String[] v = p[1].split(",");
level.lights.add(Map.entry(Box.parse(p[0]), new int[] {Integer.parseInt(v[0]), Integer.parseInt(v[1])}));
}
level.biome = biomes.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(c.getOrDefault("biome", "minecraft:plains"))));
level.biomes.clear();
for (int i = 0; c.containsKey("b" + i); i++) {
String[] p = c.get("b" + i).split("/", 2);
level.biomes.add(Map.entry(Box.parse(p[0]), biomes.getOrThrow(ResourceKey.create(Registries.BIOME, Identifier.parse(p[1])))));
}
level.sky = Integer.parseInt(c.getOrDefault("sky", "15"));
level.block = Integer.parseInt(c.getOrDefault("light", "0"));
level.darken = Integer.parseInt(c.getOrDefault("darken", "0"));
level.thunder = c.getOrDefault("thunder", "0").equals("1");
level.sea = Integer.parseInt(c.getOrDefault("sea", "63"));
level.seed = Long.parseLong(c.getOrDefault("wseed", "0"));
level.difficulty = Difficulty.byName(c.getOrDefault("difficulty", "normal"));
level.rules.set(net.minecraft.world.level.gamerules.GameRules.SPAWN_MOBS, !c.getOrDefault("rules", "1").equals("0"), null);
// The dimension and biome layers; the moon timeline's surface slime chance as a constant.
level.attributes = net.minecraft.world.attribute.EnvironmentAttributeSystem.builder().addStaticLayers(level)
.addConstantLayer(net.minecraft.world.attribute.EnvironmentAttributeMap.builder()
.set(net.minecraft.world.attribute.EnvironmentAttributes.SURFACE_SLIME_SPAWN_CHANCE, Float.parseFloat(c.getOrDefault("slime", "0.0"))).build())
.build();
level.unloaded.clear();
if (c.containsKey("unloaded")) for (String u : c.get("unloaded").split(";")) {
String[] p = u.split(",");
level.unloaded.add(ChunkPos.pack(Integer.parseInt(p[0]), Integer.parseInt(p[1])));
}
level.starts.clear();
for (int i = 0; c.containsKey("s" + i); i++) {
String[] p = c.get("s" + i).split("/", 2);
Structure structure = structureLookup.getValue(Identifier.parse(p[0]));
List<StructurePiece> pieces = new ArrayList<>();
for (String b : p[1].split(";")) {
Box bx = Box.parse(b);
pieces.add(new Piece(new BoundingBox(bx.x0(), bx.y0(), bx.z0(), bx.x1(), bx.y1(), bx.z1())));
}
level.starts.add(new Start(structure, new StructureStart(structure, new ChunkPos(0, 0), 0, new PiecesContainer(pieces))));
}
String[] r = c.getOrDefault("respawn", "0,-1000,0/minecraft:overworld").split("/");
String[] rp = r[0].split(",");
level.respawn = new LevelData.RespawnData(GlobalPos.of(ResourceKey.create(Registries.DIMENSION, Identifier.parse(r[1])),
new BlockPos(Integer.parseInt(rp[0]), Integer.parseInt(rp[1]), Integer.parseInt(rp[2]))), 0.0F, 0.0F);
level.playerList.clear();
for (int i = 0; c.containsKey("p" + i); i++) {
String[] p = c.get("p" + i).split("/");
String[] v = p[0].split(",");
OraclePlayer player = (OraclePlayer) alloc(OraclePlayer.class);
player.mode = GameType.byName(p.length > 1 ? p[1] : "survival");
entityId.setInt(player, 100000 + i);
put(player, Entity.class, "position", new Vec3(Double.parseDouble(v[0]), Double.parseDouble(v[1]), Double.parseDouble(v[2])));
level.playerList.add(player);
}
level.entities.clear();
for (int i = 0; c.containsKey("e" + i); i++) {
String[] p = c.get("e" + i).split("/");
String[] v = p[1].split(",");
Entity e = BuiltInRegistries.ENTITY_TYPE.getValue(Identifier.parse(p[0])).create(level, EntitySpawnReason.LOAD);
if (e == null) continue; // monsters do not exist in peaceful
e.setPos(Double.parseDouble(v[0]), Double.parseDouble(v[1]), Double.parseDouble(v[2]));
if (p.length > 2 && p[2].equals("p") && e instanceof Mob mob) mob.setPersistenceRequired();
level.entities.add(e);
}
RandomSource mainRandom = RandomSource.create(Long.parseLong(c.getOrDefault("seed", "0")));
put(level, Level.class, "random", mainRandom);
String mode = c.getOrDefault("mode", "chunk");
String[] cp = c.getOrDefault("chunk", "0,0").split(",");
Chunk chunk = (Chunk) alloc(Chunk.class);
chunk.level = level;
chunk.at = new ChunkPos(Integer.parseInt(cp[0]), Integer.parseInt(cp[1]));
if (mode.equals("gen")) {
level.logColumns = 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;
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));
log.add("next " + random.nextInt(1000));
} else {
Map<Long, Chunk> chunks = new HashMap<>();
NaturalSpawner.ChunkGetter getter = (key, consumer) -> {
if (level.unloaded.contains(key)) return;
Chunk k = chunks.computeIfAbsent(key, kk -> {
try {
Chunk made = (Chunk) alloc(Chunk.class);
made.level = level;
made.at = ChunkPos.unpack(kk);
return made;
} catch (Exception ex) { throw new RuntimeException(ex); }
});
consumer.accept(k);
};
var state = (NaturalSpawner.SpawnState) createState.invoke(null, Integer.parseInt(c.getOrDefault("chunkcount", "289")), level, getter, new LocalMobCapCalculator(chunkMap));
NaturalSpawner.SpawnPredicate extra = (type, lvl, pos, ch) -> {
try {
boolean ok = (boolean) canSpawn.invoke(state, type, lvl, pos, ch);
log.add("test " + BuiltInRegistries.ENTITY_TYPE.getKey(type) + " " + pos.getX() + " " + pos.getY() + " " + pos.getZ() + " " + ok);
return ok;
} catch (Exception ex) { throw new RuntimeException(ex); }
};
NaturalSpawner.AfterSpawnCallback after = (mob, ch) -> {
log.add("spawn " + BuiltInRegistries.ENTITY_TYPE.getKey(mob.getType()) + " " + bits(mob.getX()) + " " + bits(mob.getY()) + " " + bits(mob.getZ()) + " " + yaw(mob));
try { afterSpawn.invoke(state, mob, ch); } catch (Exception ex) { throw new RuntimeException(ex); }
};
if (mode.equals("pos")) {
MobCategory category = MobCategory.valueOf(c.get("cat").toUpperCase(Locale.ROOT));
String[] sp = c.get("start").split(",");
BlockPos start = new BlockPos(Integer.parseInt(sp[0]), Integer.parseInt(sp[1]), Integer.parseInt(sp[2]));
NaturalSpawner.spawnCategoryForPosition(category, level, chunk, start, extra, after);
} else {
List<MobCategory> categories = NaturalSpawner.getFilteredSpawningCategories(state, c.getOrDefault("enemies", "1").equals("1"), c.getOrDefault("persistent", "1").equals("1"));
StringBuilder names = new StringBuilder();
for (MobCategory m : categories) names.append(names.isEmpty() ? "" : ",").append(m.getSerializedName());
log.add("cats " + names);
// spawnForChunk, then spawnCategoryForChunk with its start logged.
for (MobCategory category : categories) {
boolean ok = (boolean) local.invoke(state, category, chunk.at);
log.add("local " + category.getSerializedName() + " " + ok);
if (!ok) continue;
BlockPos start = (BlockPos) randomPos.invoke(null, level, chunk);
log.add("start " + category.getSerializedName() + " " + start.getX() + " " + start.getY() + " " + start.getZ());
if (start.getY() >= level.getMinY() + 1) NaturalSpawner.spawnCategoryForPosition(category, level, chunk, start, extra, after);
}
}
log.add("next " + mainRandom.nextInt(1000));
}
w.println(c.get("name") + " " + String.join(" ; ", log));
} catch (Throwable t) {
Throwable cause = t;
while ((cause instanceof InvocationTargetException || cause instanceof RuntimeException && cause.getCause() != null) && cause.getCause() != null) cause = cause.getCause();
StringBuilder line = new StringBuilder(c.get("name") + " error " + cause);
if (System.getenv("ORACLE_TRACE") != null) for (var frame : cause.getStackTrace()) line.append("\n at ").append(frame);
w.println(line);
}
}
}
System.exit(0);
}
}
+602
View File
@@ -0,0 +1,602 @@
#!/usr/bin/env python3
"""Compare natural spawning (NaturalSpawner) with vanilla.
Java (VanillaNaturalSpawnOracle) runs vanilla's NaturalSpawner on a stand-in
ServerLevel built from each case: boxes of blocks, light and biomes, structure
starts with their pieces, existing mobs, players, the world spawn point and
unloaded chunks. Rust (natural_spawn_fixture) runs
horizon_client::mobs::spawn_selection on the same world. Three modes:
- chunk: createState over the case's mobs (category counts, spawn potential
charges, local caps), getFilteredSpawningCategories under the global caps,
then spawnForChunk (local caps, getRandomPosWithin, spawnCategoryForPosition);
- pos: spawnCategoryForPosition from a chosen start (surfaces, caves, water,
lava, structures);
- gen: spawnMobsForChunkGeneration.
Each case's log lists every attempt (the nearest player lookup), each type that
passed isValidSpawnPostitionForType with the spawn potential's answer, each
spawn with its exact position and yaw bits, the heightmap columns of
chunk-generation attempts, and the next draw of the spawner's random, which
pins every draw in between. Worlds cover the overworld, nether and end, their
biomes, caves, water and lava, block and sky light, day, night and thunder,
peaceful to hard, spawn costs (soul sand valley, warped forest), structure
spawn overrides (fortress, swamp hut, monument, outpost, trial chambers,
ancient city), mob caps, players near and far, spectators and the world spawn.
"""
import argparse
import json
import os
from pathlib import Path
import platform
import random
import subprocess
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
R = 48 # world boxes span [-R, 16 + R) around chunk (0, 0)
OVERWORLD = {
# biome: (top block, filler)
'minecraft:plains': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:forest': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:birch_forest': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:dark_forest': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:flower_forest': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:sunflower_plains': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:meadow': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:cherry_grove': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:taiga': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:old_growth_pine_taiga': ('minecraft:podzol', 'minecraft:dirt'),
'minecraft:snowy_taiga': ('minecraft:grass_block[snowy=true]', 'minecraft:dirt'),
'minecraft:snowy_plains': ('minecraft:snow_block', 'minecraft:dirt'),
'minecraft:ice_spikes': ('minecraft:snow_block', 'minecraft:packed_ice'),
'minecraft:frozen_peaks': ('minecraft:snow_block', 'minecraft:stone'),
'minecraft:jagged_peaks': ('minecraft:snow_block', 'minecraft:stone'),
'minecraft:stony_peaks': ('minecraft:stone', 'minecraft:stone'),
'minecraft:windswept_hills': ('minecraft:grass_block', 'minecraft:stone'),
'minecraft:desert': ('minecraft:sand', 'minecraft:sandstone'),
'minecraft:badlands': ('minecraft:red_sand', 'minecraft:terracotta'),
'minecraft:wooded_badlands': ('minecraft:coarse_dirt', 'minecraft:terracotta'),
'minecraft:savanna': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:jungle': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:bamboo_jungle': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:sparse_jungle': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:swamp': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:mangrove_swamp': ('minecraft:mud', 'minecraft:mud'),
'minecraft:mushroom_fields': ('minecraft:mycelium', 'minecraft:dirt'),
'minecraft:beach': ('minecraft:sand', 'minecraft:sand'),
'minecraft:snowy_beach': ('minecraft:sand', 'minecraft:sand'),
'minecraft:stony_shore': ('minecraft:stone', 'minecraft:stone'),
'minecraft:river': ('minecraft:sand', 'minecraft:dirt'),
'minecraft:pale_garden': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:grove': ('minecraft:snow_block', 'minecraft:dirt'),
'minecraft:snowy_slopes': ('minecraft:snow_block', 'minecraft:stone'),
'minecraft:savanna_plateau': ('minecraft:grass_block', 'minecraft:dirt'),
'minecraft:eroded_badlands': ('minecraft:red_sand', 'minecraft:terracotta'),
}
OCEANS = ['minecraft:ocean', 'minecraft:deep_ocean', 'minecraft:warm_ocean', 'minecraft:lukewarm_ocean',
'minecraft:deep_lukewarm_ocean', 'minecraft:cold_ocean', 'minecraft:deep_cold_ocean',
'minecraft:frozen_ocean', 'minecraft:deep_frozen_ocean', 'minecraft:river', 'minecraft:frozen_river']
CAVES = ['minecraft:lush_caves', 'minecraft:dripstone_caves', 'minecraft:deep_dark', 'minecraft:sulfur_caves']
NETHER = {
'minecraft:nether_wastes': 'minecraft:netherrack',
'minecraft:crimson_forest': 'minecraft:crimson_nylium',
'minecraft:warped_forest': 'minecraft:warped_nylium',
'minecraft:soul_sand_valley': 'minecraft:soul_sand',
'minecraft:basalt_deltas': 'minecraft:basalt',
}
END = ['minecraft:the_end', 'minecraft:end_highlands', 'minecraft:end_midlands', 'minecraft:small_end_islands',
'minecraft:end_barrens']
MOBS = {
'monster': ['zombie', 'skeleton', 'creeper', 'spider', 'enderman', 'witch', 'slime', 'drowned', 'husk', 'stray',
'zombie_villager', 'cave_spider', 'bogged', 'parched'],
'creature': ['cow', 'pig', 'sheep', 'chicken', 'horse', 'rabbit', 'wolf', 'fox', 'llama', 'polar_bear', 'goat',
'strider'],
'ambient': ['bat'],
'water_creature': ['squid', 'dolphin'],
'water_ambient': ['cod', 'salmon', 'tropical_fish', 'pufferfish'],
'axolotls': ['axolotl'],
'underground_water_creature': ['glow_squid'],
'nether': ['zombified_piglin', 'piglin', 'hoglin', 'ghast', 'magma_cube', 'skeleton', 'enderman', 'strider',
'blaze', 'wither_skeleton'],
}
CATEGORIES = ['monster', 'creature', 'ambient', 'axolotls', 'underground_water_creature', 'water_creature',
'water_ambient']
TORCH_LIGHT = [0, 0, 0, 1, 4, 7, 8, 9, 12, 14]
def java_next_int(seed, bound):
"""java.util.Random(seed).nextInt(bound)."""
mask = (1 << 48) - 1
state = (seed ^ 0x5DEECE66D) & mask
while True:
state = (state * 0x5DEECE66D + 0xB) & mask
bits = state >> 17
value = bits % bound
if bits - value + bound - 1 < 2**31:
return value
def slime_chunk(world_seed, cx, cz):
"""WorldgenRandom.seedSlimeChunk(cx, cz, seed, 987234911).nextInt(10) == 0."""
def i32(v):
return (v + 2**31) % 2**32 - 2**31
seed = (world_seed + i32(cx * cx * 4987142) + i32(cx * 5947611) + i32(cz * cz) * 4392871 + i32(cz * 389711)) ^ 987234911
return java_next_int(seed & (2**64 - 1), 10) == 0
def box(x0, y0, z0, x1, y1, z1):
return f'{x0},{y0},{z0},{x1},{y1},{z1}'
class World:
"""One case's world, written as the oracle's key=value tokens."""
def __init__(self, rng):
self.rng = rng
self.kv = {}
self.blocks, self.lights, self.biomes, self.starts, self.entities, self.players = [], [], [], [], [], []
self.sites = [] # (x, y, z, kind): open positions above a floor, in water or in lava
self.dim = 'minecraft:overworld'
self.surface = 64
self.min_y = -64
def block(self, b, state):
self.blocks.append(f'{b}/{state}')
def light(self, b, sky, block):
self.lights.append(f'{b}/{sky},{block}')
def tokens(self):
out = dict(self.kv)
for prefix, items in (('k', self.blocks), ('l', self.lights), ('b', self.biomes), ('s', self.starts),
('e', self.entities), ('p', self.players)):
for i, v in enumerate(items):
out[f'{prefix}{i}'] = v
return ' '.join(f'{k}={v}' for k, v in out.items())
def overworld(w, rng, biome=None, ocean=None):
lo, hi = -R, 16 + R - 1
w.kv['dim'] = 'minecraft:overworld'
w.kv['sea'] = 63
ocean = rng.random() < 0.2 if ocean is None else ocean
if ocean:
biome = biome or rng.choice(OCEANS)
floor = rng.randrange(28, 58)
w.kv['biome'] = biome
w.block(box(lo, -64, lo, hi, floor - 1, hi), 'minecraft:stone')
w.block(box(lo, floor - 1, lo, hi, floor - 1, hi), rng.choice(['minecraft:sand', 'minecraft:gravel', 'minecraft:clay']))
top = 62
water = 'minecraft:ice' if 'frozen' in biome and rng.random() < 0.5 else 'minecraft:water'
w.block(box(lo, floor, lo, hi, 62, hi), 'minecraft:water')
if water == 'minecraft:ice':
w.block(box(lo, 62, lo, hi, 62, hi), 'minecraft:ice')
for _ in range(6):
w.sites.append((rng.randrange(-4, 20), rng.randrange(floor, 62), rng.randrange(-4, 20), 'water'))
w.surface = 63
depth = 62 - floor
w.light(box(lo, -64, lo, hi, 62, hi), max(0, 15 - depth // 2) if rng.random() < 0.5 else 15, 0)
w.light(box(lo, -64, lo, hi, floor - 1, hi), 0, 0)
# An island.
if rng.random() < 0.3:
x, z = rng.randrange(-8, 20), rng.randrange(-8, 20)
w.block(box(x, floor, z, x + rng.randrange(3, 12), 64, z + rng.randrange(3, 12)), 'minecraft:sand')
return
biome = biome or rng.choice(list(OVERWORLD))
top, filler = OVERWORLD[biome]
surface = rng.randrange(58, 90)
w.surface = surface + 1
w.kv['biome'] = biome
w.block(box(lo, -64, lo, hi, surface - 4, hi), rng.choice(['minecraft:stone', 'minecraft:stone', 'minecraft:deepslate']))
w.block(box(lo, surface - 3, lo, hi, surface - 1, hi), filler)
w.block(box(lo, surface, lo, hi, surface, hi), top)
w.light(box(lo, -64, lo, hi, surface, hi), 0, 0)
for _ in range(4):
w.sites.append((rng.randrange(-2, 18), surface + 1, rng.randrange(-2, 18), 'surface'))
# Terraces and hills at other heights.
for _ in range(rng.randrange(0, 4)):
x, z, dy = rng.randrange(lo, hi), rng.randrange(lo, hi), rng.choice([-3, -2, -1, 1, 2, 3, 5])
sx, sz = rng.randrange(3, 20), rng.randrange(3, 20)
if dy > 0:
w.block(box(x, surface, z, x + sx, surface + dy - 1, z + sz), filler)
w.block(box(x, surface + dy, z, x + sx, surface + dy, z + sz), top)
else:
w.block(box(x, surface + dy + 1, z, x + sx, surface, z + sz), 'minecraft:air')
w.block(box(x, surface + dy, z, x + sx, surface + dy, z + sz), top)
w.light(box(x, surface + dy + 1, z, x + sx, surface, z + sz), 15, 0)
# Ponds, lava, trees and features on the surface.
for _ in range(rng.randrange(0, 3)):
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
fluid = 'minecraft:water' if rng.random() < 0.8 else 'minecraft:lava'
d = rng.randrange(1, 4)
w.block(box(x, surface - d + 1, z, x + rng.randrange(2, 9), surface, z + rng.randrange(2, 9)), fluid)
w.sites.append((x, surface, z, 'water' if fluid == 'minecraft:water' else 'lava'))
for _ in range(rng.randrange(0, 4)):
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
h = rng.randrange(4, 7)
leaves = rng.choice(['minecraft:oak_leaves', 'minecraft:spruce_leaves', 'minecraft:jungle_leaves', 'minecraft:birch_leaves'])
w.block(box(x - 2, surface + h - 2, z - 2, x + 2, surface + h + 1, z + 2), leaves)
w.block(box(x, surface + 1, z, x, surface + h, z), 'minecraft:oak_log')
w.light(box(x - 2, surface + 1, z - 2, x + 2, surface + h - 3, z + 2), 14, 0)
for _ in range(rng.randrange(0, 6)):
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
thing = rng.choice(['minecraft:short_grass', 'minecraft:tall_grass[half=lower]', 'minecraft:snow[layers=1]',
'minecraft:oak_slab[type=bottom]', 'minecraft:glass', 'minecraft:rail', 'minecraft:torch',
'minecraft:sweet_berry_bush', 'minecraft:powder_snow', 'minecraft:cobweb',
'minecraft:white_carpet', 'minecraft:oak_fence', 'minecraft:redstone_block', 'minecraft:magma_block'])
w.block(box(x, surface + 1, z, x + rng.randrange(0, 5), surface + 1, z + rng.randrange(0, 5)), thing)
# Caves: lit or dark rooms underground, some with water, a cave biome.
for _ in range(rng.choice([0, 1, 2, 2, 3, 4, 5])):
x, z = rng.randrange(-20, 14), rng.randrange(-20, 14)
y = rng.randrange(-60, surface - 6)
sx, sy, sz = rng.randrange(4, 24), rng.randrange(2, 8), rng.randrange(4, 24)
y1 = min(y + sy, surface - 2)
w.block(box(x, y, z, x + sx, y1, z + sz), 'minecraft:cave_air' if rng.random() < 0.5 else 'minecraft:air')
w.light(box(x, y, z, x + sx, y1, z + sz), 0, rng.choice(TORCH_LIGHT))
wet = rng.random() < 0.35
if wet:
w.block(box(x, y, z, x + sx, y + rng.randrange(1, 4), z + sz), 'minecraft:water')
if rng.random() < 0.3:
w.block(box(x, y - 1, z, x + sx, y - 1, z + sz), rng.choice(['minecraft:clay', 'minecraft:moss_block', 'minecraft:sculk', 'minecraft:dripstone_block', 'minecraft:clay']))
if rng.random() < 0.45:
w.biomes.append(f'{box(x - 2, y - 2, z - 2, x + sx + 2, y1 + 2, z + sz + 2)}/{rng.choice(CAVES)}')
for _ in range(3):
w.sites.append((rng.randrange(x, x + sx + 1), y + (rng.randrange(0, 2) if wet else 0), rng.randrange(z, z + sz + 1), 'water' if wet else 'cave'))
if rng.random() < 0.2:
# An opening to the sky.
w.block(box(x + 1, y1, z + 1, x + 3, surface, z + 3), 'minecraft:air')
w.light(box(x + 1, y, z + 1, x + 3, surface, z + 3), 15, 0)
def nether(w, rng, biome=None):
lo, hi = -R, 16 + R - 1
biome = biome or rng.choice(list(NETHER))
w.kv['dim'] = 'minecraft:the_nether'
w.kv['sea'] = 32
w.kv['biome'] = biome
w.kv['sky'] = 0
floor = rng.randrange(28, 80)
w.surface = 128
w.block(box(lo, 0, lo, hi, 0, hi), 'minecraft:bedrock')
w.block(box(lo, 1, lo, hi, floor - 1, hi), 'minecraft:netherrack')
w.block(box(lo, floor, lo, hi, floor, hi), NETHER[biome])
w.block(box(lo, rng.randrange(100, 120), lo, hi, 126, hi), 'minecraft:netherrack')
w.block(box(lo, 127, lo, hi, 127, hi), 'minecraft:bedrock')
w.light(box(lo, 0, lo, hi, 127, hi), 0, rng.choice([0, 0, 3, 7, 11]))
for _ in range(4):
w.sites.append((rng.randrange(-2, 18), floor + 1, rng.randrange(-2, 18), 'surface'))
if floor <= 34 or rng.random() < 0.4:
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
w.block(box(x, min(floor, 20), z, x + rng.randrange(6, 30), 31, z + rng.randrange(6, 30)), 'minecraft:lava')
w.sites.append((x + 2, 31, z + 2, 'lava'))
w.light(box(x, 20, z, x + 30, 40, z + 30), 0, 15)
for _ in range(rng.randrange(0, 4)):
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
y = rng.randrange(floor - 10, floor + 20)
w.block(box(x, y, z, x + rng.randrange(3, 14), y, z + rng.randrange(3, 14)),
rng.choice(['minecraft:netherrack', 'minecraft:nether_wart_block', 'minecraft:soul_soil', 'minecraft:magma_block',
'minecraft:glowstone', 'minecraft:blackstone', 'minecraft:nether_bricks', 'minecraft:fire']))
if rng.random() < 0.3:
x, z = rng.randrange(lo, hi), rng.randrange(lo, hi)
w.biomes.append(f'{box(x, 0, z, x + rng.randrange(8, 40), 127, z + rng.randrange(8, 40))}/{rng.choice(list(NETHER))}')
return floor
def end(w, rng):
lo, hi = -R, 16 + R - 1
w.kv['dim'] = 'minecraft:the_end'
w.kv['sea'] = 0
w.kv['sky'] = 0
w.kv['biome'] = rng.choice(END)
top = rng.randrange(40, 70)
w.surface = top + 1
w.block(box(lo, top - rng.randrange(5, 30), lo, hi, top, hi), 'minecraft:end_stone')
w.light(box(lo, 0, lo, hi, 255, hi), 0, rng.choice([0, 0, 5, 14]))
for _ in range(4):
w.sites.append((rng.randrange(-2, 18), top + 1, rng.randrange(-2, 18), 'surface'))
def structures(w, rng):
dim = w.kv['dim']
s = w.surface
if dim == 'minecraft:the_nether':
if rng.random() < 0.6:
# Fortress corridors on nether bricks (FORTRESS_ENEMIES and the piece override).
y = rng.randrange(40, 80)
pieces = []
for _ in range(rng.randrange(1, 4)):
x, z = rng.randrange(-30, 30), rng.randrange(-30, 30)
along = rng.random() < 0.5
x1, z1 = (x + rng.randrange(10, 40), z + 4) if along else (x + 4, z + rng.randrange(10, 40))
pieces.append(box(x, y, z, x1, y + 6, z1))
w.block(box(x, y, z, x1, y, z1), 'minecraft:nether_bricks')
w.block(box(x, y + 1, z, x1, y + 5, z1), 'minecraft:air')
w.light(box(x, y + 1, z, x1, y + 5, z1), 0, rng.choice([0, 7, 11]))
w.block(box(x, y + 6, z, x1, y + 6, z1), 'minecraft:nether_bricks')
w.sites.append((rng.randrange(x, x1 + 1), y + 1, rng.randrange(z, z1 + 1), 'structure'))
w.starts.append('minecraft:fortress/' + ';'.join(pieces))
if rng.random() < 0.2:
x, z = rng.randrange(-20, 20), rng.randrange(-20, 20)
w.starts.append(f'minecraft:bastion_remnant/{box(x, 30, z, x + 30, 80, z + 30)}')
return
if dim != 'minecraft:overworld':
return
biome = w.kv['biome']
roll = rng.random()
x, z = rng.randrange(-24, 24), rng.randrange(-24, 24)
if biome in ('minecraft:swamp', 'minecraft:mangrove_swamp') and roll < 0.7:
# Swamp hut: witches and cats only in its piece.
y = s + 1
w.block(box(x, y, z, x + 6, y, z + 8), 'minecraft:spruce_planks')
w.block(box(x + 1, y + 1, z + 1, x + 5, y + 4, z + 7), 'minecraft:air')
w.light(box(x + 1, y + 1, z + 1, x + 5, y + 4, z + 7), rng.choice([0, 3, 9]), 0)
w.block(box(x, y + 5, z, x + 6, y + 5, z + 8), 'minecraft:spruce_planks')
w.starts.append(f'minecraft:swamp_hut/{box(x, y, z, x + 6, y + 6, z + 8)}')
w.sites.append((x + 3, y + 1, z + 4, 'structure'))
elif 'ocean' in biome and roll < 0.6:
w.starts.append(f'minecraft:monument/{box(x, 39, z, x + 57, 61, z + 57)}')
w.sites.append((x + 8, rng.randrange(40, 61), z + 8, 'structure'))
elif roll < 0.15:
w.starts.append(f'minecraft:pillager_outpost/{box(x, s, z, x + 8, s + 20, z + 8)};{box(x + 10, s, z + 2, x + 14, s + 5, z + 6)}')
w.sites.append((x + 4, s, z + 4, 'structure'))
elif roll < 0.25:
y = rng.randrange(-40, -10)
w.block(box(x, y, z, x + 20, y + 8, z + 20), 'minecraft:air')
w.block(box(x, y - 1, z, x + 20, y - 1, z + 20), 'minecraft:tuff_bricks')
w.light(box(x, y, z, x + 20, y + 8, z + 20), 0, 0)
w.starts.append(f'minecraft:trial_chambers/{box(x, y - 1, z, x + 20, y + 9, z + 20)}')
w.sites.append((x + 5, y, z + 5, 'structure'))
elif roll < 0.32:
y = rng.randrange(-52, -30)
w.block(box(x, y, z, x + 30, y + 10, z + 30), 'minecraft:air')
w.block(box(x, y - 1, z, x + 30, y - 1, z + 30), 'minecraft:deepslate_tiles')
w.light(box(x, y, z, x + 30, y + 10, z + 30), 0, 0)
w.biomes.append(f'{box(x - 4, y - 4, z - 4, x + 34, y + 14, z + 34)}/minecraft:deep_dark')
w.starts.append(f'minecraft:ancient_city/{box(x, y - 1, z, x + 30, y + 11, z + 30)}')
w.sites.append((x + 5, y, z + 5, 'structure'))
elif roll < 0.4:
w.starts.append(f'minecraft:village_plains/{box(x, s - 2, z, x + 12, s + 8, z + 12)}')
def mob_entities(w, rng):
dim = w.kv['dim']
pool = MOBS['nether'] if dim == 'minecraft:the_nether' else sum((MOBS[c] for c in CATEGORIES if c in MOBS), [])
n = rng.choice([0, 0, 1, 3, 6, 12, 25, 80])
for _ in range(n):
t = rng.choice(pool)
x, z = rng.uniform(-R + 2, 16 + R - 2), rng.uniform(-R + 2, 16 + R - 2)
y = rng.choice([w.surface, w.surface, rng.randrange(w.min_y + 2, w.surface + 1)])
persistent = '/p' if rng.random() < 0.2 else ''
w.entities.append(f'minecraft:{t}/{x:.3f},{y},{z:.3f}{persistent}')
if w.kv.get('biome') in ('minecraft:soul_sand_valley', 'minecraft:warped_forest') or any('soul_sand_valley' in b or 'warped' in b for b in w.biomes):
for _ in range(rng.randrange(0, 10)):
t = rng.choice(['skeleton', 'ghast', 'enderman', 'strider'])
w.entities.append(f'minecraft:{t}/{rng.uniform(-10, 26):.3f},{w.sites[0][1]},{rng.uniform(-10, 26):.3f}')
# Clusters right where spawns happen (obstruction and spawn potential).
if rng.random() < 0.3:
t = rng.choice(pool)
for _ in range(rng.randrange(1, 8)):
w.entities.append(f'minecraft:{t}/{rng.randrange(0, 16) + 0.5},{w.surface},{rng.randrange(0, 16) + 0.5}')
def place_players(w, rng):
count = rng.choice([1, 1, 1, 1, 2, 3])
for _ in range(count):
kind = rng.random()
if kind < 0.75:
# 24 to ~100 blocks from the chunk: most attempts are allowed.
ang, dist = rng.uniform(0, 6.283), rng.uniform(26, 100)
elif kind < 0.87:
ang, dist = rng.uniform(0, 6.283), rng.uniform(0, 30)
else:
ang, dist = rng.uniform(0, 6.283), rng.uniform(100, 200)
import math
x, z = 8 + dist * math.cos(ang), 8 + dist * math.sin(ang)
y = w.surface + rng.choice([0, 0, 5, 30, -20])
mode = rng.choice(['survival'] * 8 + ['creative', 'adventure', 'spectator'])
w.players.append(f'{x:.3f},{y},{z:.3f}/{mode}')
def common(w, rng, seed):
w.kv['seed'] = seed
w.kv['wseed'] = rng.randrange(-2**63, 2**63)
if w.kv['dim'] == 'minecraft:overworld':
w.kv['darken'] = rng.choice([0, 0, 2, 4, 7, 11, 11, 11, 11, 11])
if rng.random() < 0.08:
w.kv['thunder'] = 1
if rng.random() < 0.3:
w.kv['slime'] = rng.choice([0.0, 0.125, 0.25, 0.5])
w.kv['difficulty'] = rng.choice(['normal', 'normal', 'hard', 'easy', 'peaceful'])
if rng.random() < 0.15:
w.kv['respawn'] = f'{rng.randrange(-20, 36)},{w.surface},{rng.randrange(-20, 36)}/{w.kv["dim"]}'
if rng.random() < 0.2:
w.kv['unloaded'] = ';'.join(f'{rng.choice([-1, 0, 1])},{rng.choice([-1, 0, 1])}' for _ in range(rng.randrange(1, 5)))
def make_world(rng, seed, dim=None, biome=None, ocean=None):
w = World(rng)
dim = dim or rng.choices(['overworld', 'nether', 'end'], [70, 22, 8])[0]
if dim == 'overworld':
overworld(w, rng, biome=biome, ocean=ocean)
elif dim == 'nether':
w.min_y = 0
nether(w, rng, biome=biome)
else:
w.min_y = 0
end(w, rng)
structures(w, rng)
mob_entities(w, rng)
place_players(w, rng)
common(w, rng, seed)
return w
def corpus():
rng = random.Random(0x4E5350)
cases = []
# Whole chunk ticks.
for k in range(3000):
w = make_world(rng, rng.randrange(-2**47, 2**47))
if rng.random() < 0.2:
w.kv['chunkcount'] = rng.choice([0, 1, 4, 17, 40, 100, 200, 289, 578])
w.kv['enemies'] = 1 if rng.random() < 0.9 else 0
w.kv['persistent'] = 1 if rng.random() < 0.5 else 0
cases.append(f'chunk/{k} mode=chunk {w.tokens()}')
# Chosen starts at sites: surfaces, cave floors, water, lava, structure floors.
for k in range(7000):
w = make_world(rng, rng.randrange(-2**47, 2**47))
dim = w.kv['dim']
x, y, z, kind = rng.choice(w.sites)
if kind == 'structure' and rng.random() < 0.5:
x, z = x + rng.randrange(-3, 4), z + rng.randrange(-3, 4)
if rng.random() < 0.1:
y = rng.randrange(w.min_y + 1, w.surface + 2)
if dim == 'minecraft:the_nether':
category = 'creature' if kind == 'lava' or rng.random() < 0.2 else 'monster'
elif dim == 'minecraft:the_end':
category = 'monster'
elif kind == 'water':
category = rng.choice(['water_creature', 'water_ambient', 'axolotls', 'underground_water_creature', 'monster'])
elif kind == 'cave':
category = rng.choice(['monster', 'monster', 'ambient', 'ambient', 'creature'])
elif kind == 'structure':
category = rng.choice(['monster', 'monster', 'creature', 'ambient', 'water_ambient'])
else:
category = rng.choice(['monster', 'creature', 'creature', 'ambient'])
if rng.random() < 0.05:
category = rng.choice(CATEGORIES)
cases.append(f'pos/{k} mode=pos cat={category} start={x},{y},{z} {w.tokens()}')
# Targeted nights and places for the rarer types.
targets = [
('minecraft:desert', False, 'surface', 'monster'), ('minecraft:desert', False, 'surface', 'creature'),
('minecraft:snowy_plains', False, 'surface', 'monster'), ('minecraft:ice_spikes', False, 'surface', 'creature'),
('minecraft:swamp', False, 'surface', 'monster'), ('minecraft:mangrove_swamp', False, 'surface', 'monster'),
('minecraft:jungle', False, 'surface', 'monster'), ('minecraft:bamboo_jungle', False, 'surface', 'monster'),
('minecraft:plains', False, 'surface', 'monster'), ('minecraft:savanna', False, 'surface', 'creature'),
('minecraft:badlands', False, 'surface', 'creature'), ('minecraft:warm_ocean', True, 'water', 'water_ambient'),
('minecraft:lukewarm_ocean', True, 'water', 'water_creature'), ('minecraft:deep_ocean', True, 'water', 'monster'),
('minecraft:frozen_ocean', True, 'water', 'creature'), ('minecraft:lush_caves', None, 'cave', 'axolotls'),
('minecraft:sulfur_caves', None, 'cave', 'monster'), ('minecraft:dripstone_caves', None, 'cave', 'monster'),
('minecraft:crimson_forest', 'nether', 'surface', 'monster'), ('slime', None, 'cave', 'monster'),
('minecraft:river', True, 'water', 'monster'),
]
for k in range(4000):
biome, ocean, kind, category = rng.choice(targets)
if ocean == 'nether':
w = make_world(rng, rng.randrange(-2**47, 2**47), dim='nether', biome=biome)
elif kind == 'cave':
w = make_world(rng, rng.randrange(-2**47, 2**47), dim='overworld', ocean=False)
else:
w = make_world(rng, rng.randrange(-2**47, 2**47), dim='overworld', biome=biome, ocean=ocean)
w.kv['darken'] = 11 if category == 'monster' else rng.choice([0, 0, 4, 11])
w.kv.pop('thunder', None)
sites = [t for t in w.sites if t[3] == kind] or w.sites
x, y, z, _ = rng.choice(sites)
if kind == 'cave':
# A dark room under the start, in the cave biome (or a slime chunk).
w.block(box(x - 6, y, z - 6, x + 6, y + 4, z + 6), 'minecraft:cave_air')
floor = 'minecraft:clay' if category == 'axolotls' else rng.choice(['minecraft:stone', 'minecraft:deepslate'])
w.block(box(x - 6, y - 1, z - 6, x + 6, y - 1, z + 6), floor)
if category == 'axolotls' or biome == 'minecraft:dripstone_caves' and rng.random() < 0.5:
w.block(box(x - 6, y, z - 6, x + 6, y + 2, z + 6), 'minecraft:water')
w.light(box(x - 6, y, z - 6, x + 6, y + 4, z + 6), 0, rng.choice([0, 0, 0, 3]))
if biome == 'slime':
y = min(y, rng.randrange(-50, 38))
w.block(box(x - 6, y, z - 6, x + 6, y + 4, z + 6), 'minecraft:cave_air')
w.block(box(x - 6, y - 1, z - 6, x + 6, y - 1, z + 6), 'minecraft:stone')
w.light(box(x - 6, y, z - 6, x + 6, y + 4, z + 6), 0, 0)
while not slime_chunk(w.kv['wseed'], 0, 0):
w.kv['wseed'] = rng.randrange(-2**63, 2**63)
else:
w.biomes.append(f'{box(x - 8, y - 3, z - 8, x + 8, y + 6, z + 8)}/{biome}')
if biome in ('minecraft:swamp', 'minecraft:mangrove_swamp'):
w.kv['slime'] = rng.choice([0.0, 0.25, 0.5])
cases.append(f'target/{k} mode=pos cat={category} start={x},{y},{z} {w.tokens()}')
# Chunk generation: creature packs placed on the surface.
for k in range(1500):
w = World(rng)
dim = rng.choices(['overworld', 'nether'], [85, 15])[0]
if dim == 'overworld':
biome = rng.choice(list(OVERWORLD) + ['minecraft:frozen_ocean', 'minecraft:beach'] * 2)
if biome in OVERWORLD:
overworld(w, rng, biome=biome, ocean=False)
else:
overworld(w, rng, biome=biome, ocean=True)
else:
w.min_y = 0
nether(w, rng)
common(w, rng, rng.randrange(-2**47, 2**47))
w.kv['gseed'] = rng.randrange(-2**47, 2**47)
if rng.random() < 0.05:
w.kv['rules'] = 0
w.kv['chunk'] = f'{rng.randrange(-3, 3)},{rng.randrange(-3, 3)}'
cases.append(f'gen/{k} mode=gen {w.tokens()}')
return cases
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prepare', action='store_true')
args = parser.parse_args()
build = lab.LOCAL / 'rust-natural-spawn-parity'
build.mkdir(parents=True, exist_ok=True)
if args.prepare:
lab.prepare()
lock, jars, libraries = base.verify_artifacts()
resources = lab.LOCAL / 'horizon-client/26.3'
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaNaturalSpawnOracle.java'
sources = [oracle, Path(__file__), lab.ROOT / 'crates/horizon-client/examples/natural_spawn_fixture.rs',
lab.ROOT / 'crates/horizon-client/src/mobs/spawn_selection.rs',
lab.ROOT / 'crates/horizon-client/src/mobs/spawn_rules.rs']
tested = base.source_hashes(sources)
inputs = build / 'inputs.txt'
cases = corpus()
inputs.write_text(''.join(line + '\n' for line in cases), encoding='utf-8')
classes = build / 'classes'
classes.mkdir(exist_ok=True)
deps = os.pathsep.join(map(str, libraries))
def run(command, name):
with (build / (name + '.log')).open('w', encoding='utf-8') as log:
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-sourcepath', str(lab.ROOT / 'mods/horizon/src/test/java'),
'-cp', str(jars['server']) + os.pathsep + deps, '-d', classes, oracle], 'compile-java')
run(['java', '-Xmx3G', '-cp', os.pathsep.join([str(classes), str(jars['server']), deps]),
'dev.horizonlab.parity.VanillaNaturalSpawnOracle', inputs, build / 'server.txt'], 'server')
run(['cargo', 'build', '--release', '-p', 'horizon-client', '--example', 'natural_spawn_fixture'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'release/examples/natural_spawn_fixture'
run([binary, resources, inputs, build / 'rust.txt'], 'rust')
vanilla = (build / 'server.txt').read_text().splitlines()
errors = [l for l in vanilla if ' error ' in l]
if errors:
raise SystemExit(f'oracle errors ({len(errors)}): {errors[:5]}')
rust = (build / 'rust.txt').read_text().splitlines()
bad = [(a, b) for a, b in zip(vanilla, rust) if a != b]
if bad or len(vanilla) != len(rust):
report = '\n'.join(f' vanilla {a[:1500]}\n rust {b[:1500]}' for a, b in bad[:20])
raise SystemExit(f'{len(bad)} of {len(vanilla)} cases differ (rust {len(rust)} lines):\n{report}')
base.ensure_sources_unchanged(sources, tested)
spawns = sum(line.count(' ; spawn ') for line in vanilla)
attempts = sum(line.count(' ; try ') + line.count(' ; col ') for line in vanilla)
tests = sum(line.count(' ; test ') for line in vanilla)
lab.write_json(build / 'report.json', {
'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(), 'cases': len(cases),
'attempts': attempts, 'type_checks_passed': tests, 'spawns': spawns, 'sources_sha256': tested,
'scope': 'NaturalSpawner: createState (counts, spawn potential, local caps), getFilteredSpawningCategories, '
'spawnForChunk, spawnCategoryForPosition (distance and respawn checks, getRandomSpawnMobAt over mobsAt: '
'fortress bounds, structure spawn overrides, biome spawns; isValidSpawnPostitionForType with '
'SpawnPlacements; isValidPositionForMob with each class\'s checkSpawnRules, checkSpawnObstruction and '
'removeWhenFarAway; cluster and group sizes) and spawnMobsForChunkGeneration, with every random draw. '
'finalizeSpawn runs with a fixed random in the oracle; its own outcomes are not compared.'})
print(f'{len(cases):,} natural spawn cases match vanilla ({attempts:,} attempts, {tests:,} type checks passed, '
f'{spawns:,} spawns)')
if __name__ == '__main__':
main()