mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 17:21:49 -10:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b56d62273 | ||
|
|
52bc81ca5b | ||
|
|
265461f2eb | ||
|
|
d6dc08098a | ||
|
|
455a9996d2 | ||
|
|
cbc469249e | ||
|
|
e436116e38 | ||
|
|
35d2781e9d | ||
|
|
66d1292d10 | ||
|
|
d3de3cd0a9 | ||
|
|
e3b168688e | ||
|
|
974e18e52a | ||
|
|
c89695dd93 | ||
|
|
0a36a6d8c6 | ||
|
|
b9501fc826 | ||
|
|
9dd9a83699 | ||
|
|
341576a084 | ||
|
|
0b0e4a8aae | ||
|
|
8acb46756b | ||
|
|
33e35d37c0 | ||
|
|
81fb4c7371 | ||
|
|
e247790c83 | ||
|
|
ceb12718cb | ||
|
|
f74d2fd5f4 | ||
|
|
7dac46ec3c | ||
|
|
8a4ebcc553 | ||
|
|
9ff9055b4f | ||
|
|
b8e729b950 | ||
|
|
55264459ce | ||
|
|
ab2bc22515 | ||
|
|
441774c229 | ||
|
|
693556ce32 | ||
|
|
dfbb106c69 | ||
|
|
38bce65c21 | ||
|
|
82d6d27567 | ||
|
|
a6da925837 | ||
|
|
4a922a5772 | ||
|
|
858f701ee8 | ||
|
|
ed847d43dc | ||
|
|
ab4c246cda | ||
|
|
7092f16a06 | ||
|
|
fdae4b895c | ||
|
|
c1f8ec81d8 | ||
|
|
2e953c625b | ||
|
|
3960c09159 | ||
|
|
4b8fecfe58 | ||
|
|
b7f4cba355 | ||
|
|
13772b4e5b | ||
|
|
4aa1f8e14a | ||
|
|
f88fba8d05 | ||
|
|
0b7fa14030 | ||
|
|
337d7d7a7a | ||
|
|
f642105e84 | ||
|
|
91d89b4abf | ||
|
|
60fc787288 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
# Generated files that tests compare byte for byte with what they generate: LF on every OS, also
|
||||
# where Git for Windows' core.autocrlf would check them out with CRLF.
|
||||
docs/launcher-commands.md text eol=lf
|
||||
docs/horizon-ctl-commands.md text eol=lf
|
||||
website/src/tokens.css text eol=lf
|
||||
@@ -1,104 +0,0 @@
|
||||
//! Full production fact loading, including file reads, interning and temporary destruction.
|
||||
//! cargo run --profile fast -p horizon-client --example block_facts_bench -- ROOT [SAMPLES]
|
||||
//! Use SAMPLES=1 with an OS peak-RSS wrapper; `digest` checks all facts
|
||||
//! and the shape sharing partition (including sharing between outlines and edges).
|
||||
use horizon_client::blocks::BlockFacts;
|
||||
use horizon_world::{DATA_VERSION, nbt::Tag, registry::BlockRegistry};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{HashMap, hash_map::DefaultHasher},
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
hint::black_box,
|
||||
path::Path,
|
||||
time::Instant,
|
||||
};
|
||||
type Result<T> = horizon_client::Result<T>;
|
||||
|
||||
fn tag(value: &Value) -> Result<Tag> {
|
||||
Ok(match value {
|
||||
Value::String(s) => Tag::String(s.as_str().into()),
|
||||
Value::Number(n) => Tag::Int(i32::try_from(n.as_i64().ok_or("registry number")?)?),
|
||||
Value::Array(a) => Tag::List(a.iter().map(tag).collect::<Result<_>>()?),
|
||||
Value::Object(o) => Tag::Compound(
|
||||
o.iter()
|
||||
.map(|(k, v)| Ok((k.as_str().into(), tag(v)?)))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
_ => return Err("registry value".into()),
|
||||
})
|
||||
}
|
||||
fn registry(root: &Path) -> Result<BlockRegistry> {
|
||||
let facts: Value = serde_json::from_slice(&fs::read(root.join("facts.json"))?)?;
|
||||
Ok(BlockRegistry::from_nbt(&Tag::compound([
|
||||
("DataVersion", Tag::Int(DATA_VERSION)),
|
||||
(
|
||||
"states",
|
||||
Tag::List(
|
||||
facts["states"]
|
||||
.as_array()
|
||||
.ok_or("registry states")?
|
||||
.iter()
|
||||
.map(|s| tag(&s[4]))
|
||||
.collect::<Result<_>>()?,
|
||||
),
|
||||
),
|
||||
("defaults", tag(&facts["defaults"])?),
|
||||
]))?)
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let root = Path::new(
|
||||
args.first()
|
||||
.ok_or("usage: block_facts_bench ROOT [SAMPLES|digest]")?,
|
||||
);
|
||||
let setup = Instant::now();
|
||||
let registry = registry(root)?;
|
||||
println!(
|
||||
"registry_setup_ms={:.3}",
|
||||
setup.elapsed().as_secs_f64() * 1000.0
|
||||
);
|
||||
if args.get(1).is_some_and(|s| s == "digest") {
|
||||
let facts = BlockFacts::load(root, ®istry)?;
|
||||
let mut hash = DefaultHasher::new();
|
||||
let mut shapes = HashMap::new();
|
||||
facts.air.hash(&mut hash);
|
||||
for state in &facts.states {
|
||||
format!("{state:?}").hash(&mut hash);
|
||||
for shape in [&state.outline, &state.outline_edges] {
|
||||
let next = shapes.len();
|
||||
shapes
|
||||
.entry((shape.as_ptr(), shape.len()))
|
||||
.or_insert(next)
|
||||
.hash(&mut hash);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"states={} shared_shapes={} digest={:016x}",
|
||||
facts.states.len(),
|
||||
shapes.len(),
|
||||
hash.finish()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let samples: usize = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
|
||||
assert!(samples > 0);
|
||||
// First load is reported separately; OS file caches are not forcibly evicted.
|
||||
let load = || -> Result<f64> {
|
||||
let start = Instant::now();
|
||||
let facts = black_box(BlockFacts::load(black_box(root), black_box(®istry))?);
|
||||
let ms = start.elapsed().as_secs_f64() * 1000.0;
|
||||
drop(facts);
|
||||
Ok(ms)
|
||||
};
|
||||
println!("first_load_ms={:.3}", load()?);
|
||||
if samples > 1 {
|
||||
load()?;
|
||||
load()?;
|
||||
let mut times = (0..samples).map(|_| load()).collect::<Result<Vec<_>>>()?;
|
||||
println!("samples_ms={times:.3?}");
|
||||
times.sort_by(f64::total_cmp);
|
||||
println!("median_ms={:.3}", times[times.len() / 2]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//! Resident chunk storage benchmark: generates a block of vanilla terrain, then
|
||||
//! reports the resident block/biome memory, the cost of lookups, meshing and
|
||||
//! lighting on it.
|
||||
//!
|
||||
//! chunk_bench <client resource dir> [--seed S] [--size N]
|
||||
#![forbid(unsafe_code)]
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, MIN_Y, SECTIONS},
|
||||
light::{LightTables, apply_lighting, light_chunk},
|
||||
mesher::{MeshTables, Mesher},
|
||||
models,
|
||||
streaming::{ChunkGenerator, Proto},
|
||||
vanillagen::VanillaTerrain,
|
||||
};
|
||||
use horizon_core::biome::BiomeZoom;
|
||||
use std::{path::PathBuf, sync::Arc, time::Instant};
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let root = PathBuf::from(args.first().ok_or("usage: chunk_bench RESOURCES")?);
|
||||
let flag = |name: &str, default: i64| -> i64 {
|
||||
args.iter()
|
||||
.position(|a| a == name)
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
};
|
||||
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
|
||||
let assets = Assets::load(&root)?;
|
||||
let render = assets.reload(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Biomes::load(&root)?;
|
||||
let tables = MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(&root, assets.registry)?,
|
||||
)?;
|
||||
let light = LightTables::load(&root, assets.registry, &facts)?;
|
||||
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, seed)?;
|
||||
let mut worker = terrain.worker();
|
||||
|
||||
// Protos for size+4 columns, decorated for the inner size+2, finished as size+2.
|
||||
let started = Instant::now();
|
||||
let outer = size + 4;
|
||||
let mut protos: Vec<Option<Proto>> = (0..outer * outer)
|
||||
.map(|i| Some(terrain.proto(&mut worker, i % outer, i / outer)))
|
||||
.collect();
|
||||
eprintln!("proto: {:.2}s", started.elapsed().as_secs_f64());
|
||||
let started = Instant::now();
|
||||
let inner = size + 2;
|
||||
for z in 1..=inner {
|
||||
for x in 1..=inner {
|
||||
let mut region: [Proto; 9] = std::array::from_fn(|k| {
|
||||
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
|
||||
protos[((z + dz) * outer + x + dx) as usize]
|
||||
.take()
|
||||
.expect("proto")
|
||||
});
|
||||
terrain.decorate(&mut worker, &mut region);
|
||||
for (k, p) in region.into_iter().enumerate() {
|
||||
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
|
||||
protos[((z + dz) * outer + x + dx) as usize] = Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!("decorate: {:.2}s", started.elapsed().as_secs_f64());
|
||||
let started = Instant::now();
|
||||
let mut chunks: Vec<Chunk> = Vec::new();
|
||||
for z in 1..=inner {
|
||||
for x in 1..=inner {
|
||||
let proto = protos[(z * outer + x) as usize].take().expect("proto");
|
||||
let mut chunk = terrain.finish(proto);
|
||||
chunk.x = x;
|
||||
chunk.z = z;
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
let finish = started.elapsed().as_secs_f64();
|
||||
println!(
|
||||
"finish (proto -> resident chunk): {:.1} ms per chunk",
|
||||
finish * 1e3 / chunks.len() as f64
|
||||
);
|
||||
|
||||
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
|
||||
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
|
||||
|
||||
// Light all inner-inner columns.
|
||||
let started = Instant::now();
|
||||
let mut lit = Vec::new();
|
||||
for z in 2..inner {
|
||||
for x in 2..inner {
|
||||
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
|
||||
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
|
||||
});
|
||||
let borrowed = horizon_client::mesher::borrow(®ion);
|
||||
lit.push(((x, z), light_chunk(&borrowed, &light, true)));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"light: {:.2} ms per column",
|
||||
started.elapsed().as_secs_f64() * 1e3 / lit.len() as f64
|
||||
);
|
||||
let mut chunks: Vec<Chunk> = chunks.iter().map(|c| (**c).clone()).collect();
|
||||
for ((x, z), sections) in lit {
|
||||
apply_lighting(&mut chunks[((z - 1) * inner + x - 1) as usize], sections);
|
||||
}
|
||||
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
|
||||
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
|
||||
|
||||
// Memory.
|
||||
let (mut bytes, mut sections, mut uniform) = (0usize, 0usize, 0usize);
|
||||
for chunk in &chunks {
|
||||
bytes += chunk.heap_bytes();
|
||||
for s in &chunk.sections {
|
||||
sections += 1;
|
||||
uniform += usize::from(s.is_uniform(s.get(0)));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"resident: {} chunks, {} sections ({} uniform), {:.1} KiB per chunk ({:.2} MiB total)",
|
||||
chunks.len(),
|
||||
sections,
|
||||
uniform,
|
||||
bytes as f64 / 1024.0 / chunks.len() as f64,
|
||||
bytes as f64 / 1048576.0
|
||||
);
|
||||
|
||||
// Meshing.
|
||||
let mut mesher = Mesher {
|
||||
nether_shading: false,
|
||||
assets: &assets,
|
||||
render: &render,
|
||||
tables: &tables,
|
||||
biomes: &biomes,
|
||||
visibility: tables.visibility.clone(),
|
||||
zoom: BiomeZoom::new(7),
|
||||
blend_radius: 2,
|
||||
smooth_lighting: true,
|
||||
cutout_leaves: true,
|
||||
cull_leaves: false,
|
||||
};
|
||||
let mut best = f64::MAX;
|
||||
let mut quads = 0usize;
|
||||
for _ in 0..3 {
|
||||
let started = Instant::now();
|
||||
let mut jobs = 0;
|
||||
quads = 0;
|
||||
for z in 2..inner {
|
||||
for x in 2..inner {
|
||||
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
|
||||
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
|
||||
});
|
||||
let borrowed = horizon_client::mesher::borrow(®ion);
|
||||
for sy in 0..SECTIONS as i32 {
|
||||
let mesh = mesher.mesh(&borrowed, sy);
|
||||
quads += (mesh.solid.len() + mesh.translucent.len()) / 4;
|
||||
jobs += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
best = best.min(started.elapsed().as_secs_f64() / f64::from(jobs));
|
||||
}
|
||||
println!(
|
||||
"mesh: {:.1} us per section ({} quads total)",
|
||||
best * 1e6,
|
||||
quads
|
||||
);
|
||||
|
||||
// Random lookups, as collision and entity queries do.
|
||||
let mut state = 0x9E37_79B9_7F4A_7C15u64;
|
||||
let started = Instant::now();
|
||||
let mut sum = 0u64;
|
||||
let lookups = 20_000_000u64;
|
||||
for _ in 0..lookups {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 7;
|
||||
state ^= state << 17;
|
||||
let cx = 2 + (state % u64::from((inner - 2) as u32)) as i32;
|
||||
let cz = 2 + ((state >> 8) % u64::from((inner - 2) as u32)) as i32;
|
||||
let chunk = &chunks[((cz - 1) * inner + cx - 1) as usize];
|
||||
let (x, y, z) = (
|
||||
(state >> 16) as i32 & 15,
|
||||
MIN_Y + ((state >> 24) % 384) as i32,
|
||||
(state >> 40) as i32 & 15,
|
||||
);
|
||||
sum += u64::from(chunk.get(x, y, z));
|
||||
}
|
||||
println!(
|
||||
"random get: {:.2} ns per lookup (checksum {sum})",
|
||||
started.elapsed().as_secs_f64() * 1e9 / lookups as f64
|
||||
);
|
||||
// Coherent scan, as a column or area query does.
|
||||
let started = Instant::now();
|
||||
let mut sum = 0u64;
|
||||
for _ in 0..4 {
|
||||
for chunk in &chunks {
|
||||
for y in MIN_Y..MIN_Y + 384 {
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
sum += u64::from(chunk.get(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"scan get: {:.2} ns per lookup (checksum {sum})",
|
||||
started.elapsed().as_secs_f64() * 1e9 / (4.0 * chunks.len() as f64 * 98304.0)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
//! Isolated production mob-model builder benchmark; no assets, Game, GPU or full-frame work.
|
||||
//! CARGO_TARGET_DIR=/home/bob/Documents/horizon/target CARGO_INCREMENTAL=0 \
|
||||
//! cargo run --profile fast -p horizon-client --example entity_geometry_bench
|
||||
use horizon_client::{
|
||||
mesher::Vertex,
|
||||
mobs::{Part, Polygon, anim::Poses, render},
|
||||
};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
|
||||
fn cube(name: String, position: f32) -> Part {
|
||||
let polygons = (0..6)
|
||||
.map(|face| {
|
||||
let axis = face / 2;
|
||||
let side = (face % 2) as f32;
|
||||
let mut normal = [0.0; 3];
|
||||
normal[axis] = side * 2.0 - 1.0;
|
||||
Polygon {
|
||||
normal,
|
||||
vertices: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]].map(|uv| {
|
||||
let mut vertex = [0.0; 5];
|
||||
vertex[axis] = side * 8.0;
|
||||
vertex[(axis + 1) % 3] = uv[0] * 8.0;
|
||||
vertex[(axis + 2) % 3] = uv[1] * 8.0;
|
||||
vertex[3] = uv[0];
|
||||
vertex[4] = uv[1];
|
||||
vertex
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Part {
|
||||
name,
|
||||
pose: [position, 0.0, 0.0, 0.1, 0.2, 0.0, 1.0, 1.0, 1.0],
|
||||
polygons,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build(model: &Part, poses: &Poses, count: usize, out: &mut Vec<Vertex>) {
|
||||
for i in 0..count {
|
||||
render::draw(
|
||||
black_box(model),
|
||||
glam::Mat4::from_translation(glam::Vec3::new((i % 32) as f32, 0.0, (i / 32) as f32)),
|
||||
black_box(poses),
|
||||
[0.125, 0.25, 0.5, 0.75],
|
||||
0xffc0_d0e0,
|
||||
0xf0f0,
|
||||
0,
|
||||
out,
|
||||
);
|
||||
}
|
||||
black_box(out.as_slice());
|
||||
}
|
||||
|
||||
fn measure(model: &Part, poses: &Poses, count: usize, iterations: usize, reuse: bool) -> f64 {
|
||||
let mut retained = Vec::new();
|
||||
build(model, poses, count, &mut retained); // Untimed warmup.
|
||||
let start = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
let mut out = if reuse {
|
||||
std::mem::take(&mut retained)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
out.clear();
|
||||
build(model, poses, count, &mut out);
|
||||
if reuse {
|
||||
retained = out;
|
||||
}
|
||||
}
|
||||
start.elapsed().as_secs_f64() * 1e6 / iterations as f64
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut model = cube("body".into(), 0.0);
|
||||
model.children = (1..6).map(|i| cube(format!("part{i}"), i as f32)).collect();
|
||||
let poses = Poses::new(&model);
|
||||
for count in [32, 256, 1024, 4096, 8192] {
|
||||
let mut expected = Vec::new();
|
||||
build(&model, &poses, count, &mut expected);
|
||||
let mut retained = Vec::new();
|
||||
build(&model, &poses, count, &mut retained);
|
||||
let (pointer, capacity) = (retained.as_ptr(), retained.capacity());
|
||||
// Populated -> empty/reset -> smaller -> populated must never leave stale vertices.
|
||||
for n in [0, count / 2, count, count] {
|
||||
retained.clear();
|
||||
build(&model, &poses, n, &mut retained);
|
||||
let mut fresh = Vec::new();
|
||||
build(&model, &poses, n, &mut fresh);
|
||||
assert_eq!(
|
||||
bytemuck::cast_slice::<Vertex, u8>(&retained),
|
||||
bytemuck::cast_slice::<Vertex, u8>(&fresh)
|
||||
);
|
||||
assert_eq!(
|
||||
(retained.as_ptr(), retained.capacity()),
|
||||
(pointer, capacity)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
bytemuck::cast_slice::<Vertex, u8>(&retained),
|
||||
bytemuck::cast_slice::<Vertex, u8>(&expected)
|
||||
);
|
||||
let iterations = (100_000 / count).clamp(40, 1000);
|
||||
let mut fresh = Vec::new();
|
||||
let mut reused = Vec::new();
|
||||
let mut paired_speedups = Vec::new();
|
||||
// Alternate order, reporting medians over eleven warmed batches.
|
||||
for batch in 0..11 {
|
||||
let mut pair = [0.0; 2];
|
||||
for reuse in if batch % 2 == 0 {
|
||||
[false, true]
|
||||
} else {
|
||||
[true, false]
|
||||
} {
|
||||
let time = measure(&model, &poses, count, iterations, reuse);
|
||||
pair[usize::from(reuse)] = time;
|
||||
if reuse {
|
||||
reused.push(time);
|
||||
} else {
|
||||
fresh.push(time);
|
||||
}
|
||||
}
|
||||
paired_speedups.push(pair[0] / pair[1]);
|
||||
}
|
||||
paired_speedups.sort_by(f64::total_cmp);
|
||||
fresh.sort_by(f64::total_cmp);
|
||||
reused.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"entities={count} vertices={} capacity={capacity} retained_bytes={} fresh_us={:.3} reused_us={:.3} speedup={:.3}x paired_speedup_median={:.3}x paired_speedup_range={:.3}..{:.3}x iterations={iterations} batches=11 byte_equivalence=pass pointer_capacity_reuse=pass",
|
||||
retained.len(),
|
||||
capacity * size_of::<Vertex>(),
|
||||
fresh[5],
|
||||
reused[5],
|
||||
fresh[5] / reused[5],
|
||||
paired_speedups[5],
|
||||
paired_speedups[0],
|
||||
paired_speedups[10]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
//! Compare the exact read/deserialize sequences used by the model loaders.
|
||||
//! Run with `cargo run -p horizon-client --profile fast --example json_readers_bench`.
|
||||
//! Optional arguments: resource directory, repetitions (default 9).
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::Debug,
|
||||
fs::{self, File},
|
||||
hint::black_box,
|
||||
io::BufReader,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
type Result<T> = horizon_client::Result<T>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Reader {
|
||||
File,
|
||||
Buffered,
|
||||
Slice,
|
||||
}
|
||||
const READERS: [Reader; 3] = [Reader::File, Reader::Buffered, Reader::Slice];
|
||||
|
||||
// These match the map types and reader expressions at all three production sites.
|
||||
macro_rules! reader {
|
||||
($name:ident, $map:ty) => {
|
||||
fn $name(path: &Path, reader: Reader) -> Result<$map> {
|
||||
Ok(match reader {
|
||||
Reader::File => serde_json::from_reader(File::open(path)?)?,
|
||||
Reader::Buffered => serde_json::from_reader(BufReader::new(File::open(path)?))?,
|
||||
Reader::Slice => serde_json::from_slice(&fs::read(path)?)?,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
reader!(prepare_facts, Map<String, Value>);
|
||||
reader!(catalog_facts, BTreeMap<String, Value>);
|
||||
|
||||
struct Fixture(PathBuf);
|
||||
impl Fixture {
|
||||
fn new() -> Result<Self> {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("horizon-json-readers-{}", std::process::id()));
|
||||
fs::create_dir(&root)?;
|
||||
Ok(Self(root))
|
||||
}
|
||||
}
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn fixtures(root: &Path) -> Result<()> {
|
||||
let states: Vec<_> = (0..56)
|
||||
.map(|i| {
|
||||
json!({"facing": (["north", "east", "south", "west"][i % 4]),
|
||||
"powered": if i / 4 % 2 == 0 { "false" } else { "true" },
|
||||
"age": (i / 8).to_string()})
|
||||
})
|
||||
.collect();
|
||||
let mut models = Map::new();
|
||||
let mut render = Map::new();
|
||||
for i in 0..1024 {
|
||||
let name = format!("minecraft:fixture_block_{i:04}");
|
||||
models.insert(
|
||||
name.clone(),
|
||||
json!({
|
||||
"schema": {"facing": {"named": ["north", "east", "south", "west"]},
|
||||
"powered": {"named": ["false", "true"]}, "age": {"integer": [0, 6]}},
|
||||
"states": states
|
||||
}),
|
||||
);
|
||||
render.insert(
|
||||
name,
|
||||
json!({"rule": "never", "can_occlude": true, "bars_tag": false}),
|
||||
);
|
||||
}
|
||||
fs::write(root.join("model-states.json"), serde_json::to_vec(&models)?)?;
|
||||
fs::write(
|
||||
root.join("render-definitions.json"),
|
||||
serde_json::to_vec(&render)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn benchmark<T: PartialEq + Debug>(
|
||||
label: &str,
|
||||
path: &Path,
|
||||
repetitions: usize,
|
||||
read: impl Fn(&Path, Reader) -> Result<T>,
|
||||
) -> Result<()> {
|
||||
let expected = read(path, Reader::File)?;
|
||||
for reader in READERS {
|
||||
assert_eq!(expected, read(path, reader)?);
|
||||
for _ in 0..2 {
|
||||
black_box(read(black_box(path), reader)?);
|
||||
}
|
||||
}
|
||||
drop(expected);
|
||||
let mut samples: [Vec<f64>; 3] = std::array::from_fn(|_| Vec::new());
|
||||
for repetition in 0..repetitions {
|
||||
// Rotate reader order to distribute drift across implementations.
|
||||
for offset in 0..3 {
|
||||
let index = (repetition + offset) % 3;
|
||||
let start = Instant::now();
|
||||
let value = black_box(read(black_box(path), READERS[index])?);
|
||||
samples[index].push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
drop(value); // Match the loaders: retain the parsed tree after loading.
|
||||
}
|
||||
}
|
||||
let median: Vec<_> = samples
|
||||
.iter_mut()
|
||||
.map(|s| {
|
||||
s.sort_by(f64::total_cmp);
|
||||
s[s.len() / 2]
|
||||
})
|
||||
.collect();
|
||||
println!(
|
||||
"{label}: {} bytes, {repetitions} samples/reader, median ms: File={:.3}, BufReader={:.3}, read+slice={:.3}; slice speedup={:.2}x",
|
||||
fs::metadata(path)?.len(),
|
||||
median[0],
|
||||
median[1],
|
||||
median[2],
|
||||
median[0] / median[2]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
let fixture = if args.is_empty() {
|
||||
Some(Fixture::new()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let root = fixture
|
||||
.as_ref()
|
||||
.map(|f| f.0.clone())
|
||||
.unwrap_or_else(|| PathBuf::from(&args[0]));
|
||||
if fixture.is_some() {
|
||||
fixtures(&root)?;
|
||||
println!(
|
||||
"Generated representative fixtures: 1024 blocks, 57344 model states; warm filesystem cache."
|
||||
);
|
||||
}
|
||||
let repetitions = args.get(1).map(|s| s.parse()).transpose()?.unwrap_or(9);
|
||||
assert!(repetitions > 0, "repetitions must be positive");
|
||||
benchmark(
|
||||
"prepare model states",
|
||||
&root.join("model-states.json"),
|
||||
repetitions,
|
||||
prepare_facts,
|
||||
)?;
|
||||
benchmark(
|
||||
"catalog model states",
|
||||
&root.join("model-states.json"),
|
||||
repetitions,
|
||||
catalog_facts,
|
||||
)?;
|
||||
benchmark(
|
||||
"render definitions",
|
||||
&root.join("render-definitions.json"),
|
||||
repetitions,
|
||||
catalog_facts,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reader_equivalence_and_errors() -> Result<()> {
|
||||
let fixture = Fixture::new()?;
|
||||
let path = fixture.0.join("test.json");
|
||||
fs::write(&path, br#"{"minecraft:stone":{"schema":{},"states":[{}]},"unicode":"\u2603","duplicate":1,"duplicate":2}"#)?;
|
||||
let expected = prepare_facts(&path, Reader::File)?;
|
||||
for reader in READERS {
|
||||
assert_eq!(expected, prepare_facts(&path, reader)?);
|
||||
assert_eq!(
|
||||
expected.iter().collect::<BTreeMap<_, _>>(),
|
||||
catalog_facts(&path, reader)?.iter().collect()
|
||||
);
|
||||
}
|
||||
for invalid in [b"{\"a\": ".as_slice(), b"{} trailing", b"{\"a\":\"\xff\"}"] {
|
||||
fs::write(&path, invalid)?;
|
||||
for reader in READERS {
|
||||
let error = prepare_facts(&path, reader).unwrap_err();
|
||||
assert!(error.downcast_ref::<serde_json::Error>().is_some());
|
||||
assert!(catalog_facts(&path, reader).is_err());
|
||||
}
|
||||
}
|
||||
fs::remove_file(&path)?;
|
||||
for reader in READERS {
|
||||
let error = prepare_facts(&path, reader).unwrap_err();
|
||||
assert_eq!(
|
||||
error.downcast_ref::<std::io::Error>().unwrap().kind(),
|
||||
std::io::ErrorKind::NotFound
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
//! Serve a small flat world on the LAN server without the game, for joining
|
||||
//! with a real client while developing the protocol:
|
||||
//! `cargo run --release --example lan_flat -- RESOURCES PORT`.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, MIN_Y},
|
||||
lan::{
|
||||
Lan,
|
||||
chunks::ChunkEncoder,
|
||||
data::SyncData,
|
||||
hub::{HostEvent, HostState, Hub, HubConfig, TickCtx},
|
||||
},
|
||||
world::World,
|
||||
};
|
||||
use horizon_protocol::packets::play::Serverbound as S;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let root = PathBuf::from(args.next().ok_or("RESOURCES PORT")?);
|
||||
let port: u16 = args.next().ok_or("port")?.parse()?;
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let state = |name: &str| assets.registry.default_state(name).map_or(0, |s| s.0);
|
||||
let sync = Arc::new(SyncData::load(&root)?);
|
||||
let biomes: Vec<String> = sync
|
||||
.dynamic
|
||||
.registries
|
||||
.get("minecraft:worldgen/biome")
|
||||
.map(|v| v.iter().map(|(n, _)| n.clone()).collect())
|
||||
.unwrap_or_default();
|
||||
let encoder = ChunkEncoder::new(
|
||||
facts
|
||||
.states
|
||||
.iter()
|
||||
.map(|s| (s.name.clone(), s.fluid_level > 0 || s.waterlogged)),
|
||||
&sync.builtin,
|
||||
&biomes,
|
||||
&biomes,
|
||||
);
|
||||
let (stone, dirt, grass) = (
|
||||
state("minecraft:stone"),
|
||||
state("minecraft:dirt"),
|
||||
state("minecraft:grass_block"),
|
||||
);
|
||||
let mut world = World::new(facts.air, 0);
|
||||
for cz in -8..=8 {
|
||||
for cx in -8..=8 {
|
||||
let mut chunk = Chunk::empty(cx, cz);
|
||||
for z in 0..16 {
|
||||
for x in 0..16 {
|
||||
for y in MIN_Y..60 {
|
||||
let v = if y == 59 {
|
||||
grass
|
||||
} else if y > 55 {
|
||||
dirt
|
||||
} else {
|
||||
stone
|
||||
};
|
||||
chunk.set(x, y, z, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
chunk.lit = true;
|
||||
world.chunks.insert((cx, cz), Arc::new(chunk));
|
||||
}
|
||||
}
|
||||
let lan = Lan::start(sync.clone(), port, "Flat - Example".into(), 8)?;
|
||||
println!("listening on {}", lan.port);
|
||||
let mut hub = Hub::new(
|
||||
lan,
|
||||
sync,
|
||||
encoder,
|
||||
HubConfig {
|
||||
default_game_mode: 1,
|
||||
guest_commands: true,
|
||||
force_game_mode: false,
|
||||
hardcore: false,
|
||||
view_distance: 8,
|
||||
simulation_distance: 8,
|
||||
dimension: "minecraft:overworld".into(),
|
||||
seed: 0,
|
||||
max_players: 8,
|
||||
dedicated: false,
|
||||
integrated: false,
|
||||
},
|
||||
);
|
||||
let mut next_entity = 100u32;
|
||||
let host = HostState {
|
||||
uuid: 42,
|
||||
name: "Host".into(),
|
||||
entity_id: 1,
|
||||
position: [0.5, 60.0, 0.5],
|
||||
yaw: 0.0,
|
||||
pitch: 0.0,
|
||||
head_yaw: 0.0,
|
||||
on_ground: true,
|
||||
game_mode: 1,
|
||||
sneaking: false,
|
||||
sprinting: false,
|
||||
equipment: Default::default(),
|
||||
health: 20.0,
|
||||
attributes: Vec::new(),
|
||||
bed: None,
|
||||
living_flags: 0,
|
||||
};
|
||||
loop {
|
||||
world.game_time += 1;
|
||||
world.day_time += 1;
|
||||
let mut ctx = TickCtx {
|
||||
world: &mut world,
|
||||
host: Some(&host),
|
||||
entities: &[],
|
||||
next_entity: &mut next_entity,
|
||||
spawn: [8.5, 60.0, 8.5],
|
||||
difficulty: 2,
|
||||
difficulty_locked: false,
|
||||
clock_rate: 1.0,
|
||||
tick_rate: 20.0,
|
||||
tick_frozen: false,
|
||||
tick_steps: (0, 0),
|
||||
collision: None,
|
||||
};
|
||||
for event in hub.tick(&mut ctx) {
|
||||
match event {
|
||||
HostEvent::Joined(conn) => hub.place_new_player(conn, &mut ctx),
|
||||
HostEvent::Packet(_, packet) => {
|
||||
if std::env::var_os("HORIZON_NET_DEBUG").is_some()
|
||||
&& !matches!(*packet, S::ClientTickEnd(_))
|
||||
{
|
||||
eprintln!("[lan] {packet:?}");
|
||||
}
|
||||
}
|
||||
HostEvent::Chat { line, .. } | HostEvent::Warn(line) | HostEvent::Error(line) => {
|
||||
println!("{line}")
|
||||
}
|
||||
HostEvent::Left(_) => {}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
//! Read-only saved chunk benchmark. All timings exclude resource/corpus setup.
|
||||
//!
|
||||
//! load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]
|
||||
//! Add --profile-worker when a sampling collector cannot sample the main thread.
|
||||
//! Reports warm filesystem SaveDir::load (read, inflate, NBT, decode and light),
|
||||
//! plus isolated inflate/NBT and native decode. No files in the save are changed.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
save::{SaveDir, SaveTables, decode_with},
|
||||
};
|
||||
use horizon_world::{nbt::Limits, region::RegionFile};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{hint::black_box, path::PathBuf, sync::Arc, time::Instant};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
if std::env::args().any(|a| a == "--profile-worker") {
|
||||
std::thread::spawn(|| run().map_err(|e| e.to_string()))
|
||||
.join()
|
||||
.expect("benchmark worker")
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let usage = "load_bench RESOURCES DIMENSION_DIR [--rounds N] [--limit N] [--checksum]";
|
||||
let root = PathBuf::from(args.first().ok_or(usage)?);
|
||||
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
|
||||
let flag = |name: &str, default: usize| -> Result<usize> {
|
||||
Ok(match args.iter().position(|a| a == name) {
|
||||
Some(i) => args.get(i + 1).ok_or(usage)?.parse()?,
|
||||
None => default,
|
||||
})
|
||||
};
|
||||
let rounds = flag("--rounds", 5)?;
|
||||
let limit = flag("--limit", 256)?;
|
||||
if rounds == 0 || limit == 0 {
|
||||
return Err("rounds and limit must be positive".into());
|
||||
}
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Biomes::load(&root)?;
|
||||
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
|
||||
let source = SaveDir::new(dimension.clone());
|
||||
let _ = source.tables.set(tables.clone());
|
||||
let mut paths: Vec<_> = std::fs::read_dir(dimension.join("region"))?
|
||||
.map(|e| e.map(|e| e.path()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
paths.retain(|p| p.extension().is_some_and(|e| e == "mca"));
|
||||
paths.sort();
|
||||
let mut corpus = Vec::new();
|
||||
'regions: for path in paths {
|
||||
let mut region = RegionFile::open(path)?;
|
||||
let mut positions: Vec<_> = region.chunks().map(|(pos, _)| pos).collect();
|
||||
positions.sort_by_key(|p| (p.x, p.z));
|
||||
for pos in positions {
|
||||
let record = region.read(pos)?.ok_or("missing record")?;
|
||||
let tag = record.nbt(Limits::default())?;
|
||||
if decode_with(&tag, assets.registry, &biomes, Some(&tables)).is_none() {
|
||||
continue;
|
||||
}
|
||||
corpus.push((pos, record, tag));
|
||||
if corpus.len() == limit {
|
||||
break 'regions;
|
||||
}
|
||||
}
|
||||
}
|
||||
if corpus.is_empty() {
|
||||
return Err("no full chunks in corpus".into());
|
||||
}
|
||||
let mut records_hash = Sha256::new();
|
||||
for (pos, record, _) in &corpus {
|
||||
records_hash.update(pos.x.to_le_bytes());
|
||||
records_hash.update(pos.z.to_le_bytes());
|
||||
records_hash.update((record.compressed.len() as u64).to_le_bytes());
|
||||
records_hash.update(&record.compressed);
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"corpus": dimension, "chunks": corpus.len(), "rounds": rounds,
|
||||
"compressed_records_sha256": format!("{:x}", records_hash.finalize()),
|
||||
"compressed_bytes": corpus.iter().map(|(_, r, _)| r.compressed.len()).sum::<usize>()})
|
||||
);
|
||||
// Prime SaveDir's region handles and the filesystem cache before measurement.
|
||||
for (pos, _, _) in &corpus {
|
||||
black_box(
|
||||
source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?,
|
||||
);
|
||||
}
|
||||
for round in 0..rounds {
|
||||
let started = Instant::now();
|
||||
for (pos, _, _) in &corpus {
|
||||
black_box(
|
||||
source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?,
|
||||
);
|
||||
}
|
||||
let load_s = started.elapsed().as_secs_f64();
|
||||
let started = Instant::now();
|
||||
for (_, record, _) in &corpus {
|
||||
black_box(record.nbt(Limits::default())?);
|
||||
}
|
||||
let nbt_s = started.elapsed().as_secs_f64();
|
||||
let started = Instant::now();
|
||||
for (_, _, tag) in &corpus {
|
||||
black_box(
|
||||
decode_with(tag, assets.registry, &biomes, Some(&tables)).ok_or("decode failed")?,
|
||||
);
|
||||
}
|
||||
let decode_s = started.elapsed().as_secs_f64();
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"round": round, "load_s": load_s,
|
||||
"inflate_nbt_s": nbt_s, "decode_s": decode_s})
|
||||
);
|
||||
}
|
||||
if args.iter().any(|a| a == "--checksum") {
|
||||
let mut hash = Sha256::new();
|
||||
let mut light_loaded = 0;
|
||||
for (pos, _, _) in &corpus {
|
||||
let chunk = source
|
||||
.load(pos.x, pos.z, assets.registry, &biomes)
|
||||
.ok_or("load failed")?;
|
||||
hash.update(pos.x.to_le_bytes());
|
||||
hash.update(pos.z.to_le_bytes());
|
||||
light_loaded += usize::from(chunk.light_loaded);
|
||||
for section in &chunk.sections {
|
||||
for i in 0..4096 {
|
||||
hash.update(section.get(i).to_le_bytes());
|
||||
hash.update([section.light(i)]);
|
||||
}
|
||||
for i in 0..64 {
|
||||
hash.update(section.biomes.get(i).to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"blocks_biomes_light_sha256": format!("{:x}", hash.finalize()),
|
||||
"light_loaded": light_loaded})
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
//! Replays `tools/test_ambient_sound_parity.py` scripts through Horizon's
|
||||
//! ambient sound handlers over its `SoundEngine`:
|
||||
//! `ambient_fixture ROOT INPUT OUTPUT`.
|
||||
#[path = "audio_common/mod.rs"]
|
||||
mod common;
|
||||
use crate::audio_common as common;
|
||||
use common::{Fake, d, f, flush};
|
||||
use horizon_client::ambience::{Ambience, AmbientContext, AmbientSounds, Mood, bubble_column_in};
|
||||
use horizon_client::sound_engine::{Bound, SoundEngine, TickContext, Ticking};
|
||||
@@ -47,8 +46,8 @@ fn sounds(a: &[&str]) -> AmbientSounds {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
|
||||
engine.listening = true;
|
||||
let mut backend = Fake::default();
|
||||
+2
-2
@@ -50,8 +50,8 @@ fn particle(k: ArrowParticle) -> &'static str {
|
||||
ArrowParticle::Splash => "splash",
|
||||
}
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err("usage: arrow_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into());
|
||||
}
|
||||
+2
-2
@@ -18,8 +18,8 @@ fn music(m: Option<&Music>) -> String {
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = std::path::Path::new(&args[1]);
|
||||
let tables = MusicTables::load(root);
|
||||
let ambient = AmbientTables::load(root);
|
||||
File renamed without changes.
+2
-2
@@ -24,8 +24,8 @@ fn db(v: &Value) -> f64 {
|
||||
f64::from_bits(u64::from_str_radix(v.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let [root, input, output, scene_json] = &args[..] else {
|
||||
return Err("usage: auto_jump_probe RESOURCES INPUT OUTPUT SCENE_JSON".into());
|
||||
};
|
||||
+2
-2
@@ -118,8 +118,8 @@ fn status(s: ShulkerStatus) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: block_entity_anim_fixture RESOURCES INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -110,8 +110,8 @@ fn intern(s: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, textures, output] = args.as_slice() else {
|
||||
return Err("usage: block_entity_render_fixture RESOURCES INPUTS TEXTURES OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -26,8 +26,8 @@ fn entity(name: &str) -> BlockEntity {
|
||||
.unwrap_or_else(|| panic!("no block entity for {name}"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1]).expect("input");
|
||||
let mut out = String::new();
|
||||
let mut entities = BlockEntities::default();
|
||||
+2
-2
@@ -8,8 +8,8 @@ use std::{
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
path::Path,
|
||||
};
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let root = Path::new(&args[1]);
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
+2
-2
@@ -51,8 +51,8 @@ impl Around for Column {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: block_transform_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -16,8 +16,8 @@ use std::{
|
||||
};
|
||||
/// The stage sprite origin the cracks carry (the oracle compares the geometry and UVs only).
|
||||
const STAGE: [f32; 2] = [0.25, 0.5];
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let root = Path::new(&args[1]);
|
||||
let assets = Assets::load(root)?;
|
||||
let render = assets.reload(root)?;
|
||||
+2
-2
@@ -323,8 +323,8 @@ fn avatar(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -70,8 +70,8 @@ fn describe(state: &BlockState) -> String {
|
||||
format!("{}|{}", state.name, props.join(","))
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: cauldron_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+3
-5
@@ -5,6 +5,7 @@
|
||||
//! the oracle's JSON form.
|
||||
//!
|
||||
//! usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT
|
||||
use crate::pack_screen_fixture;
|
||||
use horizon_client::{
|
||||
chat::{ChatAction, ChatScreen, Commands},
|
||||
command::vanilla,
|
||||
@@ -15,14 +16,11 @@ use horizon_client::{
|
||||
use serde_json::{Value, json};
|
||||
use std::{fmt::Write as _, fs, path::PathBuf};
|
||||
use winit::keyboard::KeyCode as K;
|
||||
#[path = "pack_screen_fixture.rs"]
|
||||
#[allow(dead_code)]
|
||||
mod pack_screen_fixture;
|
||||
#[path = "chat_screen_fixture/registries.rs"]
|
||||
mod registries;
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, registries, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: chat_screen_fixture RESOURCES REGISTRIES CASES OUTPUT".into());
|
||||
};
|
||||
File renamed without changes.
+2
-2
@@ -1,8 +1,8 @@
|
||||
//! CloudRenderer face selection and mesh traversal, without a GPU.
|
||||
use horizon_client::clouds;
|
||||
use std::{fmt::Write, fs};
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let mut output = String::new();
|
||||
for (case, line) in fs::read_to_string(&args[1])?.lines().enumerate() {
|
||||
let values: Vec<i32> = line
|
||||
+2
-2
@@ -132,8 +132,8 @@ fn tree(d: &Dispatcher, node: usize) -> Value {
|
||||
v
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
if args.get(1).map(String::as_str) == Some("tree") {
|
||||
let d = &vanilla::registered(false);
|
||||
let mut v = tree(d, 0);
|
||||
+2
-2
@@ -24,8 +24,8 @@ fn input_name(input: Input) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -11,8 +11,8 @@ use horizon_client::{
|
||||
use serde_json::{Value, json};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: creative_search_fixture RESOURCES CREATIVE_JSON VANILLA_JSON OUTPUT";
|
||||
let names = Names::load(Path::new(args.get(1).ok_or(usage)?));
|
||||
let creative = Creative::from_json(&serde_json::from_slice(&fs::read(
|
||||
@@ -0,0 +1,49 @@
|
||||
//! How the probe binary runs a probe. main.rs lists the probes and nothing else (tools/parity.py leaves it out of
|
||||
//! the harnesses' dependencies); code every probe relies on goes here.
|
||||
use std::process::ExitCode;
|
||||
|
||||
/// `probes! { a, b, }` declares the binary's `main`, which runs the probe module named by its first argument.
|
||||
macro_rules! probes {
|
||||
($($name:ident,)*) => {
|
||||
fn run(name: &str) -> Option<std::process::ExitCode> {
|
||||
use std::process::Termination;
|
||||
Some(match name {
|
||||
$(stringify!($name) => $name::main().report(),)*
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn main() -> std::process::ExitCode {
|
||||
dispatch::main(&[$(stringify!($name)),*], run)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The probe's command line as its own example saw it: the program, then the arguments after the probe's name.
|
||||
pub fn args() -> std::vec::IntoIter<String> {
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
if args.len() > 1 {
|
||||
args.remove(1);
|
||||
}
|
||||
args.into_iter()
|
||||
}
|
||||
|
||||
/// args(), as OsStrings.
|
||||
pub fn args_os() -> std::vec::IntoIter<std::ffi::OsString> {
|
||||
let mut args: Vec<_> = std::env::args_os().collect();
|
||||
if args.len() > 1 {
|
||||
args.remove(1);
|
||||
}
|
||||
args.into_iter()
|
||||
}
|
||||
|
||||
pub fn main(names: &[&str], run: fn(&str) -> Option<ExitCode>) -> ExitCode {
|
||||
let name = std::env::args().nth(1).unwrap_or_default();
|
||||
run(&name).unwrap_or_else(|| {
|
||||
eprintln!(
|
||||
"usage: probe <name> [arguments...]\nprobes: {}",
|
||||
names.join(" ")
|
||||
);
|
||||
ExitCode::from(2)
|
||||
})
|
||||
}
|
||||
+2
-2
@@ -14,8 +14,8 @@ fn d(v: f64) -> String {
|
||||
format!("{:016x}", v.to_bits())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
|
||||
let c: Value = serde_json::from_str(&line?)?;
|
||||
+2
-2
@@ -10,8 +10,8 @@ use horizon_client::{
|
||||
};
|
||||
use std::{fmt::Write as _, fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: enchanting_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+3
-3
@@ -3,7 +3,7 @@
|
||||
//! posed model with `mobs::render::draw` under the case's living-entity root
|
||||
//! transform and writes each drawn part's packed vertex normals.
|
||||
//!
|
||||
//! cargo run --profile fast -p horizon-client --example entity_anim_fixture -- \
|
||||
//! cargo run --profile fast -p horizon-client --example probe -- entity_anim_fixture \
|
||||
//! RESOURCES VANILLA.jsonl OUT.jsonl
|
||||
use horizon_client::mesher::Vertex;
|
||||
use horizon_client::mobs::{
|
||||
@@ -288,8 +288,8 @@ fn entity_state(mob: &mut Mob, species: Species, case: &Value) {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().skip(1).collect();
|
||||
let root = PathBuf::from(&args[0]);
|
||||
let types = MobTypes::load(&root)?;
|
||||
let animations = MobData::load_animations(&root);
|
||||
+2
-2
@@ -61,8 +61,8 @@ fn species(id: &str) -> Species {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -53,8 +53,8 @@ fn line(s: &LootStack) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: entity_death_loot_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -13,8 +13,8 @@ fn d(v: f64) -> String {
|
||||
format!("{:016x}", v.to_bits())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in BufReader::new(File::open(&args[1])?).lines().enumerate() {
|
||||
let c: Value = serde_json::from_str(&line?)?;
|
||||
+2
-2
@@ -9,8 +9,8 @@ use horizon_client::{
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Write as _, fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: environment_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -388,8 +388,8 @@ fn describe(m: &Mob, all: &[Mob], regs: &SpawnRegistries) -> String {
|
||||
t.join(" ")
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: finalize_spawn_fixture RESOURCES INPUT OUTPUT HALLOWEEN")?,
|
||||
+2
-2
@@ -11,8 +11,8 @@ use std::{
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for line in input.lines() {
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
use horizon_client::{enchant::Enchantments, item_data::parse_stack, item_tooltip::firework_lines};
|
||||
use std::{error::Error, fmt::Write, path::PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1])?;
|
||||
let root = input
|
||||
.lines()
|
||||
+2
-2
@@ -458,8 +458,8 @@ impl HandWorld for ResetWorld<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -50,8 +50,8 @@ impl LimiterClock for Script {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -291,8 +291,8 @@ fn key(c: &Value, out: &mut impl Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let mut out = BufWriter::new(File::create(&args[1])?);
|
||||
for line in BufReader::new(File::open(&args[0])?).lines() {
|
||||
let c: Value = serde_json::from_str(&line?)?;
|
||||
+2
-2
@@ -13,8 +13,8 @@ use horizon_client::{
|
||||
use serde_json::Value;
|
||||
use std::{fmt::Write as _, fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: ghost_recipe_fixture RESOURCES CASES OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
use horizon_client::command::args::GameMode;
|
||||
use horizon_client::items::{ItemStack, take_held_drop};
|
||||
|
||||
fn main() {
|
||||
pub fn main() {
|
||||
for mode in [
|
||||
GameMode::Survival,
|
||||
GameMode::Creative,
|
||||
+2
-2
@@ -10,8 +10,8 @@ use serde_json::Value;
|
||||
use std::path::Path;
|
||||
use std::{fmt::Write as _, fs};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: hotbar_fixture RESOURCES CASES OUTPUT";
|
||||
let items = Items::catalog(Path::new(args.first().and(args.get(1)).ok_or(usage)?))?;
|
||||
let known = |id: &str| items.get(id).is_some();
|
||||
+2
-2
@@ -20,8 +20,8 @@ type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
#[path = "hud_fixture/kinds.rs"]
|
||||
mod kinds;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: hud_fixture RESOURCES INPUTS OUTPUT".into());
|
||||
};
|
||||
File renamed without changes.
+2
-2
@@ -11,8 +11,8 @@ fn number(v: &Value) -> f32 {
|
||||
fn bits(v: f32) -> u32 {
|
||||
if v.is_nan() { 0x7fc00000 } else { v.to_bits() }
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let mut out = BufWriter::new(File::create(&args[1])?);
|
||||
for line in BufReader::new(File::open(&args[0])?).lines() {
|
||||
let f: Value = serde_json::from_str(&line?)?;
|
||||
+2
-2
@@ -82,8 +82,8 @@ impl TargetUseWorld for World {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
|
||||
for (i, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -14,8 +14,8 @@ fn unhex(s: &str) -> Vec<u8> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let cases = std::fs::read_to_string(&args[1]).unwrap();
|
||||
let mut out = String::new();
|
||||
let mut games: Vec<LanGame> = Vec::new();
|
||||
+3
-3
@@ -2,7 +2,7 @@
|
||||
//! server with Horizon's light engine and compare every SkyLight/BlockLight
|
||||
//! value with the arrays vanilla stored.
|
||||
//!
|
||||
//! cargo run --profile fast -p horizon-client --example light_parity -- \
|
||||
//! cargo run --profile fast -p horizon-client --example probe -- light_parity \
|
||||
//! RESOURCES DIMENSION_DIR [--nether] [--chunks X0 Z0 X1 Z1]
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
@@ -21,8 +21,8 @@ use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
type Arrays = HashMap<i32, (Option<Vec<u8>>, Option<Vec<u8>>)>;
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().skip(1).collect();
|
||||
let root = PathBuf::from(
|
||||
args.first()
|
||||
.ok_or("usage: light_parity RESOURCES DIMENSION_DIR")?,
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Every horizon-client parity probe in one binary: `probe <name> [arguments]` runs `<name>.rs` as its own
|
||||
//! example did (tools/test_*_parity.py, tools/oracle_harness.py, tools/audio_harness.py). Linking ~80 examples,
|
||||
//! each with the whole client in it, was most of the parity job's probe build; one binary builds it once.
|
||||
//!
|
||||
//! This file only lists the probes, so that adding one reruns no harness: tools/parity.py leaves it out of
|
||||
//! their dependencies. How a probe runs is in dispatch.rs.
|
||||
#[macro_use]
|
||||
mod dispatch;
|
||||
|
||||
use dispatch::{args, args_os};
|
||||
|
||||
mod ambient_fixture;
|
||||
mod arrow_probe;
|
||||
mod audio_attributes_fixture;
|
||||
/// Sound-engine fakes the audio probes share.
|
||||
mod audio_common;
|
||||
mod auto_jump_probe;
|
||||
mod block_entity_anim_fixture;
|
||||
mod block_entity_render_fixture;
|
||||
mod block_entity_tickers_fixture;
|
||||
mod block_particles_fixture;
|
||||
mod block_transform_fixture;
|
||||
mod breaking_fixture;
|
||||
mod camera_probe;
|
||||
mod cauldron_fixture;
|
||||
mod chat_screen_fixture;
|
||||
mod cloud_fixture;
|
||||
mod command_parse_fixture;
|
||||
mod container_screen_probe;
|
||||
mod creative_search_fixture;
|
||||
mod emitter_particles_probe;
|
||||
mod enchanting_fixture;
|
||||
mod entity_anim_fixture;
|
||||
mod entity_components_probe;
|
||||
mod entity_death_loot_fixture;
|
||||
mod entity_particles_probe;
|
||||
mod environment_fixture;
|
||||
mod finalize_spawn_fixture;
|
||||
mod fire_starter_sound_probe;
|
||||
mod firework_tooltip_fixture;
|
||||
mod first_person_probe;
|
||||
mod frame_limiter_probe;
|
||||
mod game_loop_probe;
|
||||
mod ghost_recipe_fixture;
|
||||
mod held_drop_fixture;
|
||||
mod hotbar_fixture;
|
||||
mod hud_fixture;
|
||||
mod input_probe;
|
||||
mod interaction_input_fixture;
|
||||
mod lan_probe;
|
||||
mod light_parity;
|
||||
mod marker_particles_probe;
|
||||
mod menu_fixture;
|
||||
mod mipmap_probe;
|
||||
mod mob_ai_probe;
|
||||
mod mob_attributes_fixture;
|
||||
mod music_fixture;
|
||||
mod natural_spawn_fixture;
|
||||
mod options_probe;
|
||||
mod pack_resources_fixture;
|
||||
mod pack_screen_fixture;
|
||||
mod pack_selection_fixture;
|
||||
mod particle_providers_probe;
|
||||
mod pick_block_data_fixture;
|
||||
mod place_recipe_fixture;
|
||||
mod player_sync_fixture;
|
||||
mod raid_fixture;
|
||||
mod random_tick_sections_probe;
|
||||
mod recipe_book_fixture;
|
||||
mod recipe_book_render_fixture;
|
||||
mod recipe_book_view_fixture;
|
||||
mod recipe_fixture;
|
||||
mod scene_collision_probe;
|
||||
mod scene_motion_probe;
|
||||
mod scoreboard_fixture;
|
||||
mod screen_layout_fixture;
|
||||
mod section_mesh_fixture;
|
||||
mod server_files_probe;
|
||||
mod situational_music_fixture;
|
||||
mod skin_image_fixture;
|
||||
mod sound_engine_fixture;
|
||||
mod sound_event_fixture;
|
||||
mod spawn_rules_fixture;
|
||||
mod sprint_probe;
|
||||
mod subtitle_fixture;
|
||||
mod tab_list_fixture;
|
||||
mod team_rules_fixture;
|
||||
mod toggle_keys_probe;
|
||||
mod tooltip_display_fixture;
|
||||
mod trading_fixture;
|
||||
mod translucency_fixture;
|
||||
mod weather_fixture;
|
||||
mod world_border_render_fixture;
|
||||
mod world_options_fixture;
|
||||
|
||||
probes! {
|
||||
ambient_fixture,
|
||||
arrow_probe,
|
||||
audio_attributes_fixture,
|
||||
auto_jump_probe,
|
||||
block_entity_anim_fixture,
|
||||
block_entity_render_fixture,
|
||||
block_entity_tickers_fixture,
|
||||
block_particles_fixture,
|
||||
block_transform_fixture,
|
||||
breaking_fixture,
|
||||
camera_probe,
|
||||
cauldron_fixture,
|
||||
chat_screen_fixture,
|
||||
cloud_fixture,
|
||||
command_parse_fixture,
|
||||
container_screen_probe,
|
||||
creative_search_fixture,
|
||||
emitter_particles_probe,
|
||||
enchanting_fixture,
|
||||
entity_anim_fixture,
|
||||
entity_components_probe,
|
||||
entity_death_loot_fixture,
|
||||
entity_particles_probe,
|
||||
environment_fixture,
|
||||
finalize_spawn_fixture,
|
||||
fire_starter_sound_probe,
|
||||
firework_tooltip_fixture,
|
||||
first_person_probe,
|
||||
frame_limiter_probe,
|
||||
game_loop_probe,
|
||||
ghost_recipe_fixture,
|
||||
held_drop_fixture,
|
||||
hotbar_fixture,
|
||||
hud_fixture,
|
||||
input_probe,
|
||||
interaction_input_fixture,
|
||||
lan_probe,
|
||||
light_parity,
|
||||
marker_particles_probe,
|
||||
menu_fixture,
|
||||
mipmap_probe,
|
||||
mob_ai_probe,
|
||||
mob_attributes_fixture,
|
||||
music_fixture,
|
||||
natural_spawn_fixture,
|
||||
options_probe,
|
||||
pack_resources_fixture,
|
||||
pack_screen_fixture,
|
||||
pack_selection_fixture,
|
||||
particle_providers_probe,
|
||||
pick_block_data_fixture,
|
||||
place_recipe_fixture,
|
||||
player_sync_fixture,
|
||||
raid_fixture,
|
||||
random_tick_sections_probe,
|
||||
recipe_book_fixture,
|
||||
recipe_book_render_fixture,
|
||||
recipe_book_view_fixture,
|
||||
recipe_fixture,
|
||||
scene_collision_probe,
|
||||
scene_motion_probe,
|
||||
scoreboard_fixture,
|
||||
screen_layout_fixture,
|
||||
section_mesh_fixture,
|
||||
server_files_probe,
|
||||
situational_music_fixture,
|
||||
skin_image_fixture,
|
||||
sound_engine_fixture,
|
||||
sound_event_fixture,
|
||||
spawn_rules_fixture,
|
||||
sprint_probe,
|
||||
subtitle_fixture,
|
||||
tab_list_fixture,
|
||||
team_rules_fixture,
|
||||
toggle_keys_probe,
|
||||
tooltip_display_fixture,
|
||||
trading_fixture,
|
||||
translucency_fixture,
|
||||
weather_fixture,
|
||||
world_border_render_fixture,
|
||||
world_options_fixture,
|
||||
}
|
||||
+2
-2
@@ -22,8 +22,8 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(".local/horizon-client/26.3");
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
+6
-4
@@ -58,11 +58,12 @@ fn state(menu: &Menu) -> String {
|
||||
.map(|(slot, stack)| format!("{slot}:{}", canon_stack(stack.as_ref())))
|
||||
.collect();
|
||||
let line = format!(
|
||||
"{} | {} |{data} | {} | {}",
|
||||
"{} | {} |{data} | {} | {} | {}",
|
||||
slots.join(" "),
|
||||
canon_stack(menu.carried.as_ref()),
|
||||
menu.drops.join(";"),
|
||||
equipped.join(";")
|
||||
equipped.join(";"),
|
||||
menu.player_sounds.join(";")
|
||||
);
|
||||
if menu.kind() != Kind::Lectern {
|
||||
return line;
|
||||
@@ -74,8 +75,8 @@ fn state(menu: &Menu) -> String {
|
||||
format!("{line} | inv{items}")
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: menu_fixture RESOURCES INPUT OUTPUT")?,
|
||||
@@ -171,6 +172,7 @@ fn main() -> horizon_client::Result<()> {
|
||||
});
|
||||
menu.drops.clear();
|
||||
menu.equipped.clear();
|
||||
menu.player_sounds.clear();
|
||||
out += &format!("init {}\n", state(&menu));
|
||||
for op in case["ops"].as_array().ok_or("ops")? {
|
||||
let slot = op[0].as_i64().unwrap_or(0) as i32;
|
||||
+2
-2
@@ -4,8 +4,8 @@
|
||||
//! rows like VanillaMipmapOracle.
|
||||
use std::{fmt::Write as _, fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().skip(1).collect();
|
||||
let input = fs::read_to_string(&args[0])?;
|
||||
let mut lines = input.lines();
|
||||
let root = Path::new(lines.next().ok_or("no resources root")?);
|
||||
+2
-2
@@ -759,8 +759,8 @@ fn villager(m: &Mob) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err("usage: mob_ai_probe RESOURCES SCENARIOS OUTPUT".into());
|
||||
}
|
||||
+2
-2
@@ -6,8 +6,8 @@ use horizon_client::mobs::{Mob, MobTypes};
|
||||
use horizon_core::geometry::Vec3;
|
||||
use std::{fmt::Write as _, fs, path::Path, sync::Arc};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: mob_attributes_fixture RESOURCES OUTPUT")?,
|
||||
+3
-4
@@ -1,7 +1,6 @@
|
||||
//! Replays `tools/test_music_parity.py` scripts through Horizon's
|
||||
//! `MusicManager` over its `SoundEngine`: `music_fixture ROOT INPUT OUTPUT`.
|
||||
#[path = "audio_common/mod.rs"]
|
||||
mod common;
|
||||
use crate::audio_common as common;
|
||||
use common::{Fake, describe, f, flush};
|
||||
use horizon_client::music::{Music, MusicFrequency, MusicManager};
|
||||
use horizon_client::sound_engine::{Bound, MUSIC, SoundEngine, TickContext};
|
||||
@@ -21,8 +20,8 @@ fn music(spec: &str) -> Option<Music> {
|
||||
))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
|
||||
engine.listening = true;
|
||||
let mut backend = Fake::default();
|
||||
+2
-2
@@ -508,8 +508,8 @@ fn yaw_text(entity: &str, v: f32) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: natural_spawn_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -7,8 +7,8 @@ use horizon_client::{
|
||||
};
|
||||
use std::{fs, io::Write};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let mut names: Vec<_> = fs::read_dir(&args[0])?
|
||||
.map(|e| e.map(|e| e.path()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
+2
-2
@@ -30,8 +30,8 @@ fn types(root: &std::path::Path, file: PathBuf, key: &str) -> String {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let input = fs::read_to_string(&args[1])?;
|
||||
let mut lines = input.lines();
|
||||
let root: Vec<&str> = lines.next().ok_or("empty input")?.split(' ').collect();
|
||||
+2
-2
@@ -66,8 +66,8 @@ pub fn encode(draws: &[Draw]) -> Value {
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: pack_screen_fixture RESOURCES CASES OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -35,8 +35,8 @@ fn lists(m: &SelectionModel) -> Value {
|
||||
json!({"unselected": side(false), "selected": side(true)})
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [inputs, output] = args.as_slice() else {
|
||||
return Err("usage: pack_selection_fixture INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -140,8 +140,8 @@ fn world(assets: &Assets, facts: &BlockFacts, floor: bool, water: bool) -> Resul
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = &Path::new(".local/horizon-client").join(horizon_core::VANILLA_VERSION);
|
||||
let assets = Assets::load(root)?;
|
||||
let facts = BlockFacts::load(root, assets.registry)?;
|
||||
+2
-2
@@ -78,8 +78,8 @@ fn case(line: &str) -> Option<String> {
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = fs::read_to_string(
|
||||
args.get(1)
|
||||
.ok_or("usage: pick_block_data_fixture INPUT OUTPUT")?,
|
||||
+2
-2
@@ -28,8 +28,8 @@ fn state(menu: &Menu) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: place_recipe_fixture RESOURCES CASES ORDERS OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -8,8 +8,8 @@ fn number(value: &Value) -> f64 {
|
||||
f64::from_bits(u64::from_str_radix(value.as_str().unwrap(), 16).unwrap())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let input = std::io::BufReader::new(std::fs::File::open(&args[1])?);
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[2])?);
|
||||
for line in input.lines() {
|
||||
+2
-2
@@ -459,8 +459,8 @@ fn run_codec(c: &BTreeMap<&str, &str>) -> Result<String, String> {
|
||||
Ok(log.join(" ; "))
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: raid_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -51,8 +51,8 @@ impl LevelAccess for Air {
|
||||
fn emit(&mut self, _: LevelEvent) {}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = std::fs::read_to_string(&args[1])?;
|
||||
let root = input
|
||||
.lines()
|
||||
+2
-2
@@ -57,8 +57,8 @@ fn compound(tag: &Tag) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: recipe_book_fixture RESOURCES CASES OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -30,8 +30,8 @@ fn round(v: f32) -> f32 {
|
||||
(v * 1e4).round() / 1e4
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: recipe_book_render_fixture RESOURCES CASES OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -15,8 +15,8 @@ use horizon_client::{
|
||||
use serde_json::Value;
|
||||
use std::{fmt::Write as _, fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: recipe_book_view_fixture RESOURCES CASES OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let cases = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -12,8 +12,8 @@ use horizon_client::{
|
||||
};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: recipe_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+3
-2
@@ -28,8 +28,8 @@ fn vector(value: &Value) -> Result<Vec3> {
|
||||
};
|
||||
Ok(Vec3::new(number(0)?, number(1)?, number(2)?))
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err("usage: scene_collision_probe RESOURCES FIXTURE_DIRECTORY RADIUS".into());
|
||||
}
|
||||
@@ -78,6 +78,7 @@ fn main() -> Result<()> {
|
||||
let mut selected = collisions::EntityCollisions {
|
||||
blocks: scene,
|
||||
entities: &entities,
|
||||
border: None,
|
||||
};
|
||||
let movement = world_movement::resolve_world_movement(
|
||||
&mut selected,
|
||||
+2
-2
@@ -233,8 +233,8 @@ fn snapshot(out: &mut impl Write, player: &PlayerMotion) -> Result<()> {
|
||||
writeln!(out, "events {}", events.join(" | "))?;
|
||||
Ok(())
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err(
|
||||
"usage: scene_motion_probe RESOURCES FIXTURE_DIRECTORY --describe|REFERENCE".into(),
|
||||
+2
-2
@@ -370,8 +370,8 @@ fn score_json(sc: &sb::Score) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let registries: Value =
|
||||
serde_json::from_str(&fs::read_to_string(&args[1]).expect("registries")).expect("json");
|
||||
let list = |k: &str| -> Vec<String> {
|
||||
+2
-2
@@ -60,8 +60,8 @@ fn draw_json(d: &ScreenDraw) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: screen_layout_fixture RESOURCES INPUT OUTPUT";
|
||||
let banners = Banners::load(Path::new(args.get(1).ok_or(usage)?));
|
||||
let input = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -170,8 +170,8 @@ fn scene(
|
||||
}
|
||||
Ok(json!({"name": row["name"], "quads": quads}))
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
if args.len() != 3 {
|
||||
return Err(
|
||||
"usage: section_mesh_fixture RESOURCES (INPUT.jsonl | facts) OUTPUT.jsonl".into(),
|
||||
+2
-2
@@ -11,8 +11,8 @@ use horizon_client::server::{
|
||||
use horizon_protocol::crypto::uuid_string;
|
||||
use std::{fmt::Write as _, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = crate::args().skip(1);
|
||||
let root = std::path::PathBuf::from(args.next().ok_or("CASES OUTPUT")?);
|
||||
let output = args.next().ok_or("OUTPUT")?;
|
||||
let mut cases: Vec<_> = std::fs::read_dir(&root)?
|
||||
+2
-2
@@ -5,8 +5,8 @@ use horizon_client::music::{BackgroundMusic, Music, MusicTables, Situation};
|
||||
use horizon_client::sound_events::normalize;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let tables = MusicTables::load(std::path::Path::new(&args[1]));
|
||||
let input = std::fs::read_to_string(&args[2]).expect("input");
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(&args[3]).expect("output"));
|
||||
+2
-2
@@ -9,8 +9,8 @@ use std::{
|
||||
};
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [inputs, output] = args.as_slice() else {
|
||||
return Err("usage: skin_image_fixture INPUTS OUTPUT".into());
|
||||
};
|
||||
+3
-4
@@ -1,8 +1,7 @@
|
||||
//! Replays `tools/test_sound_engine_parity.py` scripts through Horizon's
|
||||
//! `SoundEngine` with a backend recording what an OpenAL source would hold:
|
||||
//! `sound_engine_fixture ROOT INPUT OUTPUT`.
|
||||
#[path = "audio_common/mod.rs"]
|
||||
mod common;
|
||||
use crate::audio_common as common;
|
||||
use common::{Fake, d, describe, f, flush, result_name};
|
||||
use horizon_client::sound_engine::{
|
||||
Attenuation, Backend, CartSound, InstanceId, SoundEngine, SoundInstance, TickContext,
|
||||
@@ -12,8 +11,8 @@ use horizon_core::random::LegacyRandom;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut engine = SoundEngine::new(SoundRegistry::load(std::path::Path::new(&args[1])));
|
||||
engine.listening = true;
|
||||
let mut backend = Fake::default();
|
||||
+2
-2
@@ -9,8 +9,8 @@ fn bits(v: f32) -> String {
|
||||
format!("{:08x}", v.to_bits())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let registry = SoundRegistry::load(std::path::Path::new(&args[1]));
|
||||
let mut engine = SoundEngine::new(registry);
|
||||
let input = std::fs::read_to_string(&args[2]).expect("input");
|
||||
+2
-2
@@ -136,8 +136,8 @@ fn reason(name: &str) -> Reason {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let root = Path::new(
|
||||
args.get(1)
|
||||
.ok_or("usage: spawn_rules_fixture RESOURCES INPUT OUTPUT")?,
|
||||
+2
-2
@@ -20,8 +20,8 @@ fn keys(mask: u64) -> Keys {
|
||||
analog: None,
|
||||
}
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let mut out = BufWriter::new(File::create(&args[1])?);
|
||||
for line in BufReader::new(File::open(&args[0])?).lines() {
|
||||
let f: Value = serde_json::from_str(&line?)?;
|
||||
+2
-2
@@ -21,8 +21,8 @@ fn pose(m: [f32; 6]) -> Value {
|
||||
json!(m)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: subtitle_fixture RESOURCES INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -53,8 +53,8 @@ fn runs(t: &Text) -> Value {
|
||||
out.into_iter().map(|(r, k)| json!([r, k])).collect()
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<PathBuf> = crate::args_os().skip(1).map(PathBuf::from).collect();
|
||||
let [root, inputs, output] = args.as_slice() else {
|
||||
return Err("usage: tab_list_fixture RESOURCES INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -8,8 +8,8 @@ use horizon_client::scoreboard::{self as sb, CollisionRule, NameView, Scoreboard
|
||||
use serde_json::{Value, json};
|
||||
use std::{fs, io::Write};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut out = std::io::BufWriter::new(fs::File::create(&args[2]).expect("output"));
|
||||
for line in fs::read_to_string(&args[1]).expect("inputs").lines() {
|
||||
let c: Value = serde_json::from_str(line).expect("case");
|
||||
+2
-2
@@ -11,8 +11,8 @@ use std::{
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let input = BufReader::new(File::open(&args[1])?);
|
||||
let mut out = BufWriter::new(File::create(&args[2])?);
|
||||
for (n, line) in input.lines().enumerate() {
|
||||
+2
-2
@@ -3,8 +3,8 @@ use horizon_client::{item_tooltip::TooltipDisplay, items::ItemStack};
|
||||
use horizon_world::nbt::Tag;
|
||||
use std::{error::Error, fmt::Write};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut output = String::new();
|
||||
for line in std::fs::read_to_string(&args[1])?.lines() {
|
||||
let fields: Vec<&str> = line.split('\t').collect();
|
||||
+2
-2
@@ -231,8 +231,8 @@ fn prepare(m: &mut Merchant, gossips: &mut Gossips, kv: &HashMap<String, String>
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let usage = "usage: trading_fixture RESOURCES INPUT VANILLA_OUTPUT OUTPUT";
|
||||
let root = Path::new(args.get(1).ok_or(usage)?);
|
||||
let input = fs::read_to_string(args.get(2).ok_or(usage)?)?;
|
||||
+2
-2
@@ -14,8 +14,8 @@ struct Case {
|
||||
cameras: Vec<[f64; 3]>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().skip(1).collect();
|
||||
let [input, output] = args.as_slice() else {
|
||||
return Err("usage: translucency_fixture INPUT OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -8,8 +8,8 @@ fn bits(value: f32) -> String {
|
||||
format!("{:x}", value.to_bits())
|
||||
}
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = crate::args().collect();
|
||||
let mut output = String::new();
|
||||
let mut parameters = weather::ParameterCache::default();
|
||||
for (case, line) in fs::read_to_string(&args[1])?.lines().enumerate() {
|
||||
+2
-2
@@ -24,8 +24,8 @@ fn doubles(v: &[f64]) -> Value {
|
||||
v.iter().map(|f| f.to_bits() as i64).collect()
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Vec<String> = crate::args().skip(1).collect();
|
||||
let [inputs, output] = args.as_slice() else {
|
||||
return Err("usage: world_border_render_fixture INPUTS OUTPUT".into());
|
||||
};
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
use horizon_protocol::{buf::Reader, packets::play::*};
|
||||
use std::{error::Error, fmt::Write};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = crate::args().collect();
|
||||
let mut output = String::new();
|
||||
for line in std::fs::read_to_string(&args[1])?.lines() {
|
||||
let fields: Vec<&str> = line.split('\t').collect();
|
||||
@@ -1,128 +0,0 @@
|
||||
//! The region writer on real chunks, without a window: how long a world's
|
||||
//! chunks take to be encoded, compressed and written by `SaveDir`, against
|
||||
//! the same encoding and compression done one chunk after another (what the
|
||||
//! writer thread did alone before it had helpers).
|
||||
//!
|
||||
//! save_bench RESOURCES DIMENSION_DIR [--rounds N]
|
||||
//!
|
||||
//! DIMENSION_DIR is a saved dimension folder (`saves/W/dimensions/minecraft/
|
||||
//! overworld`); its chunks are read once, then each round saves them all into
|
||||
//! a scratch folder with `save_later` + `flush`, and times the one-thread
|
||||
//! baseline. The written chunks are checked byte for byte against the
|
||||
//! baseline's compressed records.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
save::{SaveDir, SaveTables, encode_with},
|
||||
};
|
||||
use horizon_core::position::ChunkPos;
|
||||
use horizon_world::{
|
||||
compression::Compression,
|
||||
nbt::Limits,
|
||||
region::{ChunkRecord, RegionFile},
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc, time::Instant};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let usage = "usage: save_bench RESOURCES DIMENSION_DIR [--rounds N]";
|
||||
let root = PathBuf::from(args.first().ok_or(usage)?);
|
||||
let dimension = PathBuf::from(args.get(1).ok_or(usage)?);
|
||||
let rounds: usize = args
|
||||
.iter()
|
||||
.position(|a| a == "--rounds")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5);
|
||||
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Arc::new(Biomes::load(&root)?);
|
||||
let tables = Arc::new(SaveTables::new(&assets).with_block_entities(&facts));
|
||||
let registry = assets.registry;
|
||||
|
||||
let source = SaveDir::new(dimension.clone());
|
||||
let _ = source.tables.set(tables.clone());
|
||||
let mut chunks = Vec::new();
|
||||
for entry in std::fs::read_dir(dimension.join("region"))? {
|
||||
let path = entry?.path();
|
||||
if path.extension().is_none_or(|e| e != "mca") {
|
||||
continue;
|
||||
}
|
||||
let region = RegionFile::open(&path)?;
|
||||
for (pos, _) in region.chunks() {
|
||||
if let Some(chunk) = source.load(pos.x, pos.z, registry, &biomes) {
|
||||
chunks.push(Arc::new(chunk));
|
||||
}
|
||||
}
|
||||
}
|
||||
chunks.sort_by_key(|c| (c.x, c.z));
|
||||
println!("{} chunks from {}", chunks.len(), dimension.display());
|
||||
|
||||
let scratch = std::env::temp_dir().join(format!("save_bench_{}", std::process::id()));
|
||||
// Encoding and compressing on one thread (what the writer did alone),
|
||||
// then SaveDir's writer with its helpers.
|
||||
let mut times = [Vec::new(), Vec::new(), Vec::new()];
|
||||
let mut written = Vec::new();
|
||||
for round in 0..rounds {
|
||||
let started = Instant::now();
|
||||
let tags: Vec<_> = chunks
|
||||
.iter()
|
||||
.map(|c| encode_with(c, registry, &biomes, Some(&tables), true))
|
||||
.collect();
|
||||
times[0].push(started.elapsed().as_secs_f64());
|
||||
let started = Instant::now();
|
||||
let baseline: Vec<ChunkRecord> = tags
|
||||
.iter()
|
||||
.map(|tag| ChunkRecord::from_nbt(tag, Compression::Zlib, 0, Limits::default()))
|
||||
.collect::<std::io::Result<_>>()?;
|
||||
times[1].push(started.elapsed().as_secs_f64());
|
||||
drop(tags);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
let save = SaveDir::new(scratch.clone());
|
||||
let _ = save.tables.set(tables.clone());
|
||||
let started = Instant::now();
|
||||
save.save_later(&chunks, registry, &biomes)?;
|
||||
save.flush().unwrap();
|
||||
times[2].push(started.elapsed().as_secs_f64());
|
||||
drop(save);
|
||||
|
||||
if round == 0 {
|
||||
for (chunk, want) in chunks.iter().zip(&baseline) {
|
||||
let path = scratch.join(format!("region/r.{}.{}.mca", chunk.x >> 5, chunk.z >> 5));
|
||||
let got = RegionFile::open(&path)?
|
||||
.read(ChunkPos::new(chunk.x, chunk.z))?
|
||||
.ok_or("a saved chunk is missing")?;
|
||||
if got.compressed != want.compressed {
|
||||
return Err(format!("chunk {} {} differs", chunk.x, chunk.z).into());
|
||||
}
|
||||
}
|
||||
written = baseline.iter().map(|r| r.compressed.len()).collect();
|
||||
}
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
println!(
|
||||
"{} KiB compressed, identical to the one-thread records",
|
||||
written.iter().sum::<usize>() / 1024
|
||||
);
|
||||
let per_chunk = 1000.0 / chunks.len() as f64;
|
||||
let names = [
|
||||
"one thread: encode",
|
||||
"one thread: compress",
|
||||
"SaveDir (encode, compress, write)",
|
||||
];
|
||||
for (name, t) in names.iter().zip(&mut times) {
|
||||
t.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"{name}: best {:.0} ms, median {:.0} ms of {rounds} ({:.3} ms per chunk at best)",
|
||||
t[0] * 1000.0,
|
||||
t[t.len() / 2] * 1000.0,
|
||||
t[0] * per_chunk
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
//! Differential save test: load every full chunk of a world written by the
|
||||
//! vanilla 26.3 server, light it, encode it again with Horizon's chunk writer
|
||||
//! and compare the result with the original NBT: block states, biomes,
|
||||
//! heightmaps, light layers (by value), block entities and scheduled ticks.
|
||||
//!
|
||||
//! cargo run --profile fast -p horizon-client --example save_parity -- \
|
||||
//! RESOURCES DIMENSION_DIR [--nether]
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
chunk::{Chunk, MIN_Y, SECTIONS},
|
||||
light::{LightTables, apply_lighting, light_chunk},
|
||||
save::{self, SaveTables},
|
||||
};
|
||||
use horizon_core::position::ChunkPos;
|
||||
use horizon_world::{
|
||||
chunk::BlockState,
|
||||
nbt::{Limits, NamedTag, Tag},
|
||||
region::RegionFile,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
/// Block state names per section index, decoded independently of Horizon.
|
||||
fn block_names(tag: &Tag) -> BTreeMap<i64, Vec<String>> {
|
||||
let mut out = BTreeMap::new();
|
||||
let Some(Tag::List(sections)) = tag.get("sections") else {
|
||||
return out;
|
||||
};
|
||||
for s in sections {
|
||||
let Some(Tag::Byte(y)) = s.get("Y") else {
|
||||
continue;
|
||||
};
|
||||
let Some(states) = s.get("block_states") else {
|
||||
continue;
|
||||
};
|
||||
let Some(Tag::List(palette)) = states.get("palette") else {
|
||||
continue;
|
||||
};
|
||||
let names: Vec<String> = palette
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let st = BlockState::from_nbt(p).expect("palette entry");
|
||||
format!("{}{:?}", st.name, st.properties)
|
||||
})
|
||||
.collect();
|
||||
let bits = (usize::BITS - (names.len().max(1) - 1).leading_zeros()).max(4);
|
||||
let per = 64 / bits as usize;
|
||||
let data = match states.get("data") {
|
||||
Some(Tag::LongArray(d)) => d.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let blocks = (0..4096)
|
||||
.map(|i| {
|
||||
if names.len() == 1 {
|
||||
return names[0].clone();
|
||||
}
|
||||
let v = (data[i / per] as u64 >> ((i % per) as u32 * bits)) & ((1 << bits) - 1);
|
||||
names[v as usize].clone()
|
||||
})
|
||||
.collect();
|
||||
out.insert(i64::from(*y), blocks);
|
||||
}
|
||||
out
|
||||
}
|
||||
fn light(tag: &Tag, key: &str) -> HashMap<i64, Vec<u8>> {
|
||||
let mut out = HashMap::new();
|
||||
if let Some(Tag::List(sections)) = tag.get("sections") {
|
||||
for s in sections {
|
||||
if let (Some(Tag::Byte(y)), Some(Tag::ByteArray(a))) = (s.get("Y"), s.get(key)) {
|
||||
out.insert(i64::from(*y), a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Light value with vanilla's reading of missing layers.
|
||||
fn value(
|
||||
layers: &HashMap<i64, Vec<u8>>,
|
||||
sy: i64,
|
||||
i: usize,
|
||||
sky: bool,
|
||||
empty: bool,
|
||||
top: i64,
|
||||
) -> u8 {
|
||||
let nibble = |a: &Vec<u8>, i: usize| (a[i >> 1] >> ((i & 1) * 4)) & 15;
|
||||
if let Some(a) = layers.get(&sy) {
|
||||
return nibble(a, i);
|
||||
}
|
||||
if !sky || !empty {
|
||||
return 0;
|
||||
}
|
||||
((sy + 1)..=top)
|
||||
.find_map(|y| layers.get(&y))
|
||||
.map_or(15, |a| nibble(a, i & 255))
|
||||
}
|
||||
fn count(tag: &Tag, key: &str) -> usize {
|
||||
match tag.get(key) {
|
||||
Some(Tag::List(l)) => l.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> horizon_client::Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let root = PathBuf::from(
|
||||
args.first()
|
||||
.ok_or("usage: save_parity RESOURCES DIMENSION_DIR")?,
|
||||
);
|
||||
let dim = PathBuf::from(args.get(1).ok_or("missing dimension directory")?);
|
||||
let sky_light = !args.iter().any(|a| a == "--nether");
|
||||
let assets = Assets::load(&root)?;
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Biomes::load(&root)?;
|
||||
let lights = LightTables::load(&root, assets.registry, &facts)?;
|
||||
let tables = SaveTables::new(&assets).with_block_entities(&facts);
|
||||
let mut originals: HashMap<(i32, i32), NamedTag> = HashMap::new();
|
||||
let mut chunks: HashMap<(i32, i32), Chunk> = HashMap::new();
|
||||
for entry in std::fs::read_dir(dim.join("region"))? {
|
||||
let mut region = RegionFile::open(entry?.path())?;
|
||||
let positions: Vec<ChunkPos> = region.chunks().map(|(p, _)| p).collect();
|
||||
for p in positions {
|
||||
let Some(record) = region.read(p)? else {
|
||||
continue;
|
||||
};
|
||||
let tag = record.nbt(Limits::default())?;
|
||||
if tag.tag.get("isLightOn") != Some(&Tag::Byte(1)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(c) = save::decode_with(&tag, assets.registry, &biomes, Some(&tables)) {
|
||||
chunks.insert((c.x, c.z), c);
|
||||
originals.insert((p.x, p.z), tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut keys: Vec<_> = chunks
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|&(x, z)| {
|
||||
(-1..=1).all(|dz| (-1..=1).all(|dx| chunks.contains_key(&(x + dx, z + dz))))
|
||||
})
|
||||
.collect();
|
||||
keys.sort_unstable();
|
||||
let (
|
||||
mut blocks_bad,
|
||||
mut biomes_bad,
|
||||
mut heightmaps_bad,
|
||||
mut light_bad,
|
||||
mut light_total,
|
||||
mut other_bad,
|
||||
) = (0, 0, 0, 0u64, 0u64, 0);
|
||||
for &(x, z) in &keys {
|
||||
let region: [[Option<&Chunk>; 3]; 3] = std::array::from_fn(|dz| {
|
||||
std::array::from_fn(|dx| chunks.get(&(x + dx as i32 - 1, z + dz as i32 - 1)))
|
||||
});
|
||||
let mut chunk = chunks[&(x, z)].clone();
|
||||
apply_lighting(&mut chunk, light_chunk(®ion, &lights, sky_light));
|
||||
chunk.lit = true;
|
||||
let ours = save::encode_with(&chunk, assets.registry, &biomes, Some(&tables), sky_light);
|
||||
let theirs = &originals[&(x, z)];
|
||||
let (a, b) = (&ours.tag, &theirs.tag);
|
||||
// Block states: compare every non-empty vanilla section.
|
||||
let (na, nb) = (block_names(a), block_names(b));
|
||||
for (y, vb) in &nb {
|
||||
match na.get(y) {
|
||||
Some(va) if va == vb => {}
|
||||
Some(va) => {
|
||||
let n = va.iter().zip(vb).filter(|(p, q)| p != q).count();
|
||||
if blocks_bad < 5 {
|
||||
let i = va.iter().zip(vb).position(|(p, q)| p != q).unwrap_or(0);
|
||||
eprintln!(
|
||||
"chunk {x} {z} section {y}: {n} blocks differ, e.g. {} vs {}",
|
||||
va[i], vb[i]
|
||||
);
|
||||
}
|
||||
blocks_bad += 1;
|
||||
}
|
||||
None if vb.iter().all(|s| s.starts_with("minecraft:air")) => {}
|
||||
None => blocks_bad += 1,
|
||||
}
|
||||
}
|
||||
let biome_of = |t: &Tag| -> Vec<Tag> {
|
||||
match t.get("sections") {
|
||||
Some(Tag::List(l)) => l.iter().filter_map(|s| s.get("biomes").cloned()).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
};
|
||||
if biome_of(a) != biome_of(b) {
|
||||
biomes_bad += 1;
|
||||
}
|
||||
for kind in [
|
||||
"WORLD_SURFACE",
|
||||
"OCEAN_FLOOR",
|
||||
"MOTION_BLOCKING",
|
||||
"MOTION_BLOCKING_NO_LEAVES",
|
||||
] {
|
||||
let get = |t: &Tag| t.get("Heightmaps").and_then(|h| h.get(kind)).cloned();
|
||||
if get(a) != get(b) {
|
||||
if heightmaps_bad < 5 {
|
||||
eprintln!("chunk {x} {z}: heightmap {kind} differs");
|
||||
}
|
||||
heightmaps_bad += 1;
|
||||
}
|
||||
}
|
||||
for (key, sky) in [("SkyLight", true), ("BlockLight", false)] {
|
||||
if sky && !sky_light {
|
||||
continue;
|
||||
}
|
||||
let (la, lb) = (light(a, key), light(b, key));
|
||||
let top = i64::from((MIN_Y >> 4) + SECTIONS as i32);
|
||||
for si in 0..SECTIONS {
|
||||
let sy = i64::from((MIN_Y >> 4) + si as i32);
|
||||
let empty = chunk.sections[si].is_uniform(tables_air(&assets));
|
||||
for i in 0..4096 {
|
||||
light_total += 1;
|
||||
if value(&la, sy, i, sky, empty, top) != value(&lb, sy, i, sky, empty, top) {
|
||||
light_bad += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for key in ["block_entities", "block_ticks", "fluid_ticks"] {
|
||||
if count(a, key) != count(b, key) {
|
||||
if other_bad < 5 {
|
||||
eprintln!(
|
||||
"chunk {x} {z}: {key} {} vs {}",
|
||||
count(a, key),
|
||||
count(b, key)
|
||||
);
|
||||
}
|
||||
other_bad += 1;
|
||||
}
|
||||
}
|
||||
for key in [
|
||||
"structures",
|
||||
"PostProcessing",
|
||||
"InhabitedTime",
|
||||
"Status",
|
||||
"DataVersion",
|
||||
"yPos",
|
||||
] {
|
||||
if a.get(key) != b.get(key) {
|
||||
if other_bad < 5 {
|
||||
eprintln!("chunk {x} {z}: {key} differs");
|
||||
}
|
||||
other_bad += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"{} chunks: {blocks_bad} sections with different blocks, {biomes_bad} chunks with different biomes, \
|
||||
{heightmaps_bad} different heightmaps, {light_bad} of {light_total} light values differ, {other_bad} other differences",
|
||||
keys.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
fn tables_air(assets: &Assets) -> u32 {
|
||||
assets
|
||||
.registry
|
||||
.default_state("minecraft:air")
|
||||
.map_or(0, |s| s.0)
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
//! The chunk pipeline (generation, decoration, lighting, meshing) on its worker
|
||||
//! pool, without a window: how fast a render distance fills with N workers, and
|
||||
//! what the render thread pays for it in `Streaming::update`.
|
||||
//!
|
||||
//! streaming_bench RESOURCES [--threads N] [--radius R] [--seed S] [--fly CHUNKS_PER_S] [--seconds T] [--hz F]
|
||||
//! [--dimension overworld|the_nether|the_end] [--center-x X] [--center-z Z]
|
||||
//!
|
||||
//! First the area around (0, 0) fills; then, with `--fly`, the centre moves along +x
|
||||
//! at that speed for T seconds while frames run at F per second. Each phase reports
|
||||
//! its wall time, chunks generated and sections meshed, and the time `update`
|
||||
//! took on the calling thread (the render thread in the game): total, median,
|
||||
//! p99 and slowest. Compile with HORIZON_BENCH_COUNT_ALLOCATIONS=1 to count the
|
||||
//! allocations made on the calling thread during updates and by everything.
|
||||
use horizon_client::{
|
||||
assets::Assets,
|
||||
biome::Biomes,
|
||||
blocks::BlockFacts,
|
||||
light::LightTables,
|
||||
mesher::MeshTables,
|
||||
models,
|
||||
streaming::{Streaming, WorkerContext},
|
||||
vanillagen::VanillaTerrain,
|
||||
world::World,
|
||||
};
|
||||
use std::{
|
||||
alloc::{GlobalAlloc, Layout, System},
|
||||
cell::Cell,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering::Relaxed},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const TRACK: bool = option_env!("HORIZON_BENCH_COUNT_ALLOCATIONS").is_some();
|
||||
static ALLOCATIONS: AtomicU64 = AtomicU64::new(0);
|
||||
static BYTES: AtomicU64 = AtomicU64::new(0);
|
||||
thread_local! {
|
||||
static LOCAL: Cell<(u64, u64)> = const { Cell::new((0, 0)) };
|
||||
}
|
||||
fn count(size: usize) {
|
||||
if TRACK {
|
||||
ALLOCATIONS.fetch_add(1, Relaxed);
|
||||
BYTES.fetch_add(size as u64, Relaxed);
|
||||
let _ = LOCAL.try_with(|c| {
|
||||
let (n, b) = c.get();
|
||||
c.set((n + 1, b + size as u64));
|
||||
});
|
||||
}
|
||||
}
|
||||
struct CountingAllocator;
|
||||
// Delegates every operation and layout unchanged to System.
|
||||
unsafe impl GlobalAlloc for CountingAllocator {
|
||||
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
|
||||
count(layout.size());
|
||||
unsafe { System.alloc(layout) }
|
||||
}
|
||||
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
|
||||
count(layout.size());
|
||||
unsafe { System.alloc_zeroed(layout) }
|
||||
}
|
||||
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
|
||||
unsafe { System.dealloc(ptr, layout) };
|
||||
}
|
||||
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, size: usize) -> *mut u8 {
|
||||
count(size);
|
||||
unsafe { System.realloc(ptr, layout, size) }
|
||||
}
|
||||
}
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: CountingAllocator = CountingAllocator;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
fn cpu_seconds() -> f64 {
|
||||
std::fs::read_to_string("/proc/self/stat")
|
||||
.ok()
|
||||
.and_then(|stat| {
|
||||
let fields = stat
|
||||
.rsplit_once(')')?
|
||||
.1
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>();
|
||||
Some((fields[11].parse::<f64>().ok()? + fields[12].parse::<f64>().ok()?) / 100.0)
|
||||
})
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Phase {
|
||||
updates: Vec<f64>,
|
||||
local: (u64, u64),
|
||||
uploads: usize,
|
||||
stage_samples: [u64; 7],
|
||||
cpu_start: f64,
|
||||
}
|
||||
impl Phase {
|
||||
fn update(&mut self, streaming: &mut Streaming, world: &mut World) {
|
||||
if self.updates.is_empty() {
|
||||
self.cpu_start = cpu_seconds();
|
||||
}
|
||||
let before = LOCAL.with(Cell::get);
|
||||
let started = Instant::now();
|
||||
let (uploads, _) = streaming.update(world);
|
||||
self.updates.push(started.elapsed().as_secs_f64() * 1e3);
|
||||
let after = LOCAL.with(Cell::get);
|
||||
self.local.0 += after.0 - before.0;
|
||||
self.local.1 += after.1 - before.1;
|
||||
self.uploads += uploads.len();
|
||||
drop(uploads);
|
||||
for (sum, (_, count)) in self.stage_samples.iter_mut().zip(streaming.stages()) {
|
||||
*sum += count as u64;
|
||||
}
|
||||
}
|
||||
fn report(&self, name: &str, wall: Duration, streaming: &Streaming, generated: u64) {
|
||||
let mut sorted = self.updates.clone();
|
||||
sorted.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| sorted[((sorted.len() as f64 * p) as usize).min(sorted.len() - 1)];
|
||||
let total: f64 = sorted.iter().sum();
|
||||
println!(
|
||||
"{name}: {:.2} s, {} chunks generated, {} sections meshed; update {:.1} ms total over {} calls, p50 {:.3} p99 {:.3} max {:.2} ms",
|
||||
wall.as_secs_f64(),
|
||||
streaming.generated_total - generated,
|
||||
self.uploads,
|
||||
total,
|
||||
sorted.len(),
|
||||
at(0.5),
|
||||
at(0.99),
|
||||
sorted.last().copied().unwrap_or(0.0),
|
||||
);
|
||||
println!(
|
||||
"BENCH {}",
|
||||
serde_json::json!({
|
||||
"phase": name, "wall_s": wall.as_secs_f64(),
|
||||
"cpu_s": cpu_seconds() - self.cpu_start,
|
||||
"chunks": streaming.generated_total - generated, "sections": self.uploads,
|
||||
"updates": self.updates.len(), "update_ms": total,
|
||||
"mean_stages": self.stage_samples.map(|n| n as f64 / self.updates.len() as f64),
|
||||
})
|
||||
);
|
||||
if TRACK {
|
||||
println!(
|
||||
" update allocations: {} ({:.1} MiB)",
|
||||
self.local.0,
|
||||
self.local.1 as f64 / 1048576.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Nothing queued or in flight. Sections at the edge of the area stay dirty
|
||||
/// (their outer neighbours are never lit), so they don't count.
|
||||
fn settled(streaming: &Streaming) -> bool {
|
||||
streaming
|
||||
.stages()
|
||||
.iter()
|
||||
.all(|&(name, n)| name == "protos" || n == 0)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let root = PathBuf::from(args.first().ok_or("usage: streaming_bench RESOURCES")?);
|
||||
let flag = |name: &str, default: f64| -> f64 {
|
||||
args.iter()
|
||||
.position(|a| a == name)
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
};
|
||||
let auto = std::thread::available_parallelism().map_or(2, |n| n.get() - 1);
|
||||
let threads = flag("--threads", auto as f64) as usize;
|
||||
let radius = flag("--radius", 8.0) as i32;
|
||||
let seed = flag("--seed", 1234.0) as i64;
|
||||
let fly = flag("--fly", 0.0);
|
||||
let seconds = flag("--seconds", 10.0);
|
||||
let hz = flag("--hz", 240.0);
|
||||
let dimension = args
|
||||
.iter()
|
||||
.position(|a| a == "--dimension")
|
||||
.and_then(|i| args.get(i + 1))
|
||||
.map(|d| {
|
||||
if d.contains(':') {
|
||||
d.clone()
|
||||
} else {
|
||||
format!("minecraft:{d}")
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "minecraft:overworld".into());
|
||||
let center = (
|
||||
flag("--center-x", 0.0) as i32,
|
||||
flag("--center-z", 0.0) as i32,
|
||||
);
|
||||
|
||||
let assets = Arc::new(Assets::load(&root)?);
|
||||
let render = Arc::new(assets.reload(&root)?);
|
||||
let facts = BlockFacts::load(&root, assets.registry)?;
|
||||
let biomes = Arc::new(Biomes::load(&root)?);
|
||||
let tables = Arc::new(MeshTables::new(
|
||||
&assets,
|
||||
&render,
|
||||
&facts,
|
||||
models::load_visibility(&root, assets.registry)?,
|
||||
)?);
|
||||
let light = Arc::new(LightTables::load(&root, assets.registry, &facts)?);
|
||||
let terrain = Arc::new(VanillaTerrain::for_dimension(
|
||||
&root.join("worldgen"),
|
||||
assets.registry,
|
||||
&biomes,
|
||||
seed,
|
||||
&dimension,
|
||||
)?);
|
||||
let context = Arc::new(WorkerContext {
|
||||
assets,
|
||||
render: render.clone(),
|
||||
tables,
|
||||
biomes,
|
||||
light,
|
||||
});
|
||||
let mut world = World::new(facts.air, seed);
|
||||
let mut streaming = Streaming::new(terrain, context, threads);
|
||||
streaming.radius = radius;
|
||||
streaming.center = center;
|
||||
streaming.nether_shading = dimension == "minecraft:the_nether";
|
||||
streaming.sky_light = dimension != "minecraft:the_nether";
|
||||
println!(
|
||||
"{threads} workers, radius {radius}, seed {seed}, dimension {dimension}, center {center:?}"
|
||||
);
|
||||
let (allocations, bytes) = (ALLOCATIONS.load(Relaxed), BYTES.load(Relaxed));
|
||||
|
||||
// Fill: update as often as the game would uncapped, until settled.
|
||||
let mut phase = Phase::default();
|
||||
let started = Instant::now();
|
||||
let mut quiet = 0;
|
||||
while quiet < 20 {
|
||||
let uploads = phase.uploads;
|
||||
phase.update(&mut streaming, &mut world);
|
||||
quiet = if settled(&streaming) && phase.uploads == uploads {
|
||||
quiet + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
std::thread::sleep(Duration::from_micros(500));
|
||||
}
|
||||
phase.report("fill", started.elapsed(), &streaming, 0);
|
||||
|
||||
if fly > 0.0 {
|
||||
let mut phase = Phase::default();
|
||||
let generated = streaming.generated_total;
|
||||
let frame = Duration::from_secs_f64(1.0 / hz);
|
||||
let started = Instant::now();
|
||||
let mut behind = Vec::new();
|
||||
while started.elapsed().as_secs_f64() < seconds {
|
||||
let next = Instant::now() + frame;
|
||||
let x = center.0 + (started.elapsed().as_secs_f64() * fly) as i32;
|
||||
streaming.center = (x, center.1);
|
||||
phase.update(&mut streaming, &mut world);
|
||||
// Columns within 2 chunks of the player still missing their meshes.
|
||||
let near = (-2..=2)
|
||||
.flat_map(|dz| (-2..=2).map(move |dx| (x + dx, center.1 + dz)))
|
||||
.filter(|&(cx, cz)| !streaming.is_lit(cx, cz))
|
||||
.count();
|
||||
behind.push(near);
|
||||
if let Some(wait) = next.checked_duration_since(Instant::now()) {
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
}
|
||||
phase.report("fly", started.elapsed(), &streaming, generated);
|
||||
let unlit = behind.iter().filter(|&&n| n > 0).count();
|
||||
println!(
|
||||
" frames with an unlit column within 2 chunks of the player: {unlit} of {} ({:.1}%)",
|
||||
behind.len(),
|
||||
unlit as f64 * 100.0 / behind.len().max(1) as f64
|
||||
);
|
||||
}
|
||||
if TRACK {
|
||||
println!(
|
||||
"all threads: {} allocations ({:.1} MiB)",
|
||||
ALLOCATIONS.load(Relaxed) - allocations,
|
||||
(BYTES.load(Relaxed) - bytes) as f64 / 1048576.0
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
//! Resident-key synchronization used by Game (registration callback work excluded).
|
||||
//! Run: CARGO_INCREMENTAL=0 cargo run -p horizon-client --profile fast --example tick_chunk_membership_bench
|
||||
use horizon_client::{
|
||||
chunk::Chunk, chunk_map::ChunkMap, tick_chunk_membership::TickChunkMembership,
|
||||
};
|
||||
use std::{collections::HashSet, hint::black_box, sync::Arc, time::Instant};
|
||||
|
||||
type Key = (i32, i32);
|
||||
#[derive(Default)]
|
||||
struct Old {
|
||||
keys: HashSet<Key>,
|
||||
}
|
||||
impl Old {
|
||||
fn sync(&mut self, map: &ChunkMap) -> (usize, usize) {
|
||||
let loaded: HashSet<_> = map.keys().copied().collect();
|
||||
let mut counts = (0, 0);
|
||||
for key in loaded.difference(&self.keys.clone()) {
|
||||
black_box(key);
|
||||
counts.0 += 1;
|
||||
}
|
||||
for key in self.keys.difference(&loaded) {
|
||||
black_box(key);
|
||||
counts.1 += 1;
|
||||
}
|
||||
self.keys = loaded;
|
||||
counts
|
||||
}
|
||||
}
|
||||
fn new_sync(cache: &mut TickChunkMembership, map: &ChunkMap) -> (usize, usize) {
|
||||
if !cache.synchronize(map) {
|
||||
return (0, 0);
|
||||
}
|
||||
let added = cache.take_added();
|
||||
for key in &added {
|
||||
black_box(key);
|
||||
}
|
||||
for key in cache.removed() {
|
||||
black_box(key);
|
||||
}
|
||||
let counts = (added.len(), cache.removed().len());
|
||||
cache.recycle_added(added);
|
||||
counts
|
||||
}
|
||||
fn map(offset: i32) -> ChunkMap {
|
||||
let mut map = ChunkMap::default();
|
||||
// Share a payload: the synchronization path reads only keys, not block data.
|
||||
let chunk = Arc::new(Chunk::empty(0, 0));
|
||||
for x in -16..=16 {
|
||||
for z in -16..=16 {
|
||||
map.insert((x + offset, z), chunk.clone());
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
fn run(new: bool, scenario: usize, iterations: usize) -> (f64, (usize, usize)) {
|
||||
let mut a = map(0);
|
||||
let b = map(33);
|
||||
let chunk = Arc::new(Chunk::empty(0, 0));
|
||||
let mut old = Old::default();
|
||||
let mut cache = TickChunkMembership::default();
|
||||
for _ in 0..64 {
|
||||
if new {
|
||||
black_box(new_sync(&mut cache, &a));
|
||||
} else {
|
||||
black_box(old.sync(&a));
|
||||
}
|
||||
}
|
||||
let mut total = (0, 0);
|
||||
let start = Instant::now();
|
||||
for i in 0..iterations {
|
||||
let resident = match scenario {
|
||||
0 => &a,
|
||||
1 => {
|
||||
let (previous, next) = if i % 2 == 0 {
|
||||
((-16, -16), (-17, -16))
|
||||
} else {
|
||||
((-17, -16), (-16, -16))
|
||||
};
|
||||
a.remove(&previous);
|
||||
a.insert(next, chunk.clone());
|
||||
&a
|
||||
}
|
||||
2 => {
|
||||
if i % 2 == 0 {
|
||||
&b
|
||||
} else {
|
||||
&a
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let counts = if new {
|
||||
new_sync(black_box(&mut cache), black_box(resident))
|
||||
} else {
|
||||
old.sync(black_box(resident))
|
||||
};
|
||||
total.0 += counts.0;
|
||||
total.1 += counts.1;
|
||||
}
|
||||
(
|
||||
start.elapsed().as_secs_f64() * 1e9 / iterations as f64,
|
||||
black_box(total),
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
println!("1089 keys, 64 warmup calls, 9 samples; ns/sync, median");
|
||||
for (scenario, name, iterations) in [
|
||||
(0, "stable", 10_000),
|
||||
(1, "one-key same-count replacement", 2_000),
|
||||
(2, "whole-map replacement", 1_000),
|
||||
] {
|
||||
let mut before = Vec::new();
|
||||
let mut after = Vec::new();
|
||||
for sample in 0..9 {
|
||||
let (old, new) = if sample % 2 == 0 {
|
||||
(
|
||||
run(false, scenario, iterations),
|
||||
run(true, scenario, iterations),
|
||||
)
|
||||
} else {
|
||||
let new = run(true, scenario, iterations);
|
||||
(run(false, scenario, iterations), new)
|
||||
};
|
||||
assert_eq!(old.1, new.1);
|
||||
let expected = match scenario {
|
||||
0 => (0, 0),
|
||||
1 => (iterations, iterations),
|
||||
_ => (1089 * iterations, 1089 * iterations),
|
||||
};
|
||||
assert_eq!(new.1, expected);
|
||||
before.push(old.0);
|
||||
after.push(new.0);
|
||||
}
|
||||
before.sort_by(f64::total_cmp);
|
||||
after.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"{name}: {:.1} -> {:.1} ns, {:.2}x; {iterations} iterations/sample",
|
||||
before[4],
|
||||
after[4],
|
||||
before[4] / after[4]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -708,6 +708,13 @@ impl Audio {
|
||||
let seed = self.random.next_long();
|
||||
self.play_seeded(event, at, volume, pitch, seed);
|
||||
}
|
||||
/// `play` for this client only: a sound a packet's handler plays
|
||||
/// (`playLocalSound`), which the guests play from that packet themselves.
|
||||
pub fn play_unshared(&mut self, event: &str, at: Option<[f64; 3]>, volume: f32, pitch: f32) {
|
||||
let capture = self.capture.take();
|
||||
self.play(event, at, volume, pitch);
|
||||
self.capture = capture;
|
||||
}
|
||||
/// `LivingEntity.handleDamageEvent`'s pitch, `(r - r) * 0.2 + 1`, drawn on the
|
||||
/// client (the entity's own random there, never the server's world random).
|
||||
pub fn hurt_pitch(&mut self) -> f32 {
|
||||
|
||||
@@ -46,7 +46,7 @@ impl BossBar {
|
||||
style: "progress".into(),
|
||||
value: 0,
|
||||
max: 100,
|
||||
visible: false,
|
||||
visible: true,
|
||||
player: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
//! `BundleContents.Mutable`: what a bundle takes in and gives back, by
|
||||
//! weight. The menus (`menu`, `lan::menu`) run `BundleItem`'s click
|
||||
//! overrides on it.
|
||||
use crate::items::{ItemStack, Items};
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
/// A bundle weight: vanilla's `Fraction`, exact and kept reduced.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Weight {
|
||||
n: i128,
|
||||
d: i128,
|
||||
}
|
||||
|
||||
fn gcd(a: i128, b: i128) -> i128 {
|
||||
let (mut a, mut b) = (a.abs(), b.abs());
|
||||
while b != 0 {
|
||||
(a, b) = (b, a % b);
|
||||
}
|
||||
a.max(1)
|
||||
}
|
||||
|
||||
impl Weight {
|
||||
const ZERO: Self = Self { n: 0, d: 1 };
|
||||
const ONE: Self = Self { n: 1, d: 1 };
|
||||
|
||||
fn new(n: i128, d: i128) -> Self {
|
||||
let g = gcd(n, d);
|
||||
Self { n: n / g, d: d / g }
|
||||
}
|
||||
|
||||
fn add(self, o: Self) -> Self {
|
||||
Self::new(self.n * o.d + o.n * self.d, self.d * o.d)
|
||||
}
|
||||
|
||||
fn sub(self, o: Self) -> Self {
|
||||
self.add(Self { n: -o.n, d: o.d })
|
||||
}
|
||||
|
||||
fn times(self, count: u32) -> Self {
|
||||
Self::new(self.n * i128::from(count), self.d)
|
||||
}
|
||||
|
||||
/// `this.divideBy(item).intValue()` (truncated), at least 0.
|
||||
fn how_many(self, item: Self) -> u32 {
|
||||
let q = (self.n * item.d) / (self.d * item.n);
|
||||
u32::try_from(q.max(0)).unwrap_or(u32::MAX)
|
||||
}
|
||||
}
|
||||
|
||||
/// The sounds of `BundleItem` (`Entity.playSound` at the player): insert
|
||||
/// and remove one at volume 0.8 and pitch `0.8 + random * 0.4`, a failed
|
||||
/// insert at 1, 1.
|
||||
pub const INSERT: &str = "minecraft:item.bundle.insert";
|
||||
pub const INSERT_FAIL: &str = "minecraft:item.bundle.insert_fail";
|
||||
pub const REMOVE_ONE: &str = "minecraft:item.bundle.remove_one";
|
||||
|
||||
fn max_stack(items: &Items, stack: &ItemStack) -> u32 {
|
||||
items.get(&stack.item).map_or(64, |i| i.max_stack).max(1)
|
||||
}
|
||||
|
||||
/// The stack's `bundle_contents`, when it has the component.
|
||||
fn contents_of<'a>(items: &Items, stack: &'a ItemStack) -> Option<&'a [ItemStack]> {
|
||||
let inside = stack.data().map_or(&[][..], |d| &d.bundle_contents[..]);
|
||||
let bundle = items.get(&stack.item).is_some_and(|i| i.bundle);
|
||||
(bundle || !inside.is_empty()).then_some(inside)
|
||||
}
|
||||
|
||||
/// `BundleContents.getWeight`: a nested bundle weighs its contents plus
|
||||
/// 1/16, a beehive with bees 1, the rest 1 / max stack size.
|
||||
fn item_weight(items: &Items, stack: &ItemStack) -> Weight {
|
||||
if let Some(inside) = contents_of(items, stack) {
|
||||
return contents_weight(items, inside).add(Weight::new(1, 16));
|
||||
}
|
||||
let bees = stack
|
||||
.data()
|
||||
.and_then(|d| d.other("minecraft:bees"))
|
||||
.is_some_and(|b| matches!(b, Tag::List(l) if !l.is_empty()));
|
||||
if bees {
|
||||
Weight::ONE
|
||||
} else {
|
||||
Weight::new(1, i128::from(max_stack(items, stack)))
|
||||
}
|
||||
}
|
||||
|
||||
fn contents_weight(items: &Items, inside: &[ItemStack]) -> Weight {
|
||||
inside.iter().fold(Weight::ZERO, |w, s| {
|
||||
w.add(item_weight(items, s).times(s.count))
|
||||
})
|
||||
}
|
||||
|
||||
/// `BundleContents.canItemBeInBundle`: shulker boxes don't fit.
|
||||
pub fn can_be_in_bundle(stack: &ItemStack) -> bool {
|
||||
stack.count > 0 && !stack.item.ends_with("shulker_box")
|
||||
}
|
||||
|
||||
/// `BundleContents.Mutable`.
|
||||
pub struct Contents {
|
||||
items: Vec<ItemStack>,
|
||||
weight: Weight,
|
||||
}
|
||||
|
||||
impl Contents {
|
||||
/// The bundle's contents; None when the stack is no bundle.
|
||||
pub fn of(items: &Items, stack: &ItemStack) -> Option<Self> {
|
||||
let inside = contents_of(items, stack)?.to_vec();
|
||||
let weight = contents_weight(items, &inside);
|
||||
Some(Self {
|
||||
items: inside,
|
||||
weight,
|
||||
})
|
||||
}
|
||||
|
||||
/// `getMaxAmountToAdd` for `stack`'s kind of item.
|
||||
pub fn room_for(&self, items: &Items, stack: &ItemStack) -> u32 {
|
||||
Weight::ONE
|
||||
.sub(self.weight)
|
||||
.how_many(item_weight(items, stack))
|
||||
}
|
||||
|
||||
/// `tryInsert`: what fits moves to the front, merged with its kind.
|
||||
pub fn try_insert(&mut self, items: &Items, stack: &mut ItemStack) -> u32 {
|
||||
if !can_be_in_bundle(stack) {
|
||||
return 0;
|
||||
}
|
||||
let weight = item_weight(items, stack);
|
||||
let amount = stack
|
||||
.count
|
||||
.min(Weight::ONE.sub(self.weight).how_many(weight));
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
self.weight = self.weight.add(weight.times(amount));
|
||||
// ItemStack.isStackable: a damaged stack is damageable.
|
||||
let stackable = max_stack(items, stack) > 1 && stack.damage == 0;
|
||||
let same = self
|
||||
.items
|
||||
.iter()
|
||||
.position(|s| stackable && s.same_item_same_components(stack));
|
||||
let added = match same {
|
||||
Some(i) => {
|
||||
let mut merged = self.items.remove(i);
|
||||
merged.count += amount;
|
||||
stack.count -= amount;
|
||||
merged
|
||||
}
|
||||
None => {
|
||||
let mut part = stack.clone();
|
||||
part.count = amount;
|
||||
stack.count -= amount;
|
||||
part
|
||||
}
|
||||
};
|
||||
self.items.insert(0, added);
|
||||
amount
|
||||
}
|
||||
|
||||
/// `removeOne`: the first stack (Horizon keeps no selected item).
|
||||
pub fn remove_one(&mut self, items: &Items) -> Option<ItemStack> {
|
||||
if self.items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let stack = self.items.remove(0);
|
||||
self.weight = self
|
||||
.weight
|
||||
.sub(item_weight(items, &stack).times(stack.count));
|
||||
Some(stack)
|
||||
}
|
||||
|
||||
/// `toImmutable`, written back into the bundle's component.
|
||||
pub fn store(self, bundle: &mut ItemStack) {
|
||||
bundle.data_mut().bundle_contents = self.items;
|
||||
bundle.tidy();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Weight;
|
||||
|
||||
#[test]
|
||||
fn weights_add_up_exactly() {
|
||||
let w = Weight::new(1, 64)
|
||||
.times(32)
|
||||
.add(Weight::new(1, 16).times(4));
|
||||
assert_eq!(w, Weight::new(3, 4));
|
||||
assert_eq!(Weight::ONE.sub(w).how_many(Weight::new(1, 64)), 16);
|
||||
assert_eq!(Weight::ONE.sub(Weight::ONE).how_many(Weight::ONE), 0);
|
||||
// An over-full bundle (made by a command) has no room.
|
||||
let over = Weight::ONE.sub(Weight::new(5, 4));
|
||||
assert_eq!(over.how_many(Weight::new(1, 64)), 0);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 586 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user