Merge branch 'parity-base' into parity/saves
# Conflicts: # README.md
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user