diff --git a/Cargo.lock b/Cargo.lock index 5a8da580f..b1fabec8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -700,6 +700,9 @@ dependencies = [ [[package]] name = "horizon-core" version = "0.1.0" +dependencies = [ + "libm", +] [[package]] name = "horizon-protocol" diff --git a/crates/horizon-client/src/border_render.rs b/crates/horizon-client/src/border_render.rs index 422989920..ff3e7c8b3 100644 --- a/crates/horizon-client/src/border_render.rs +++ b/crates/horizon-client/src/border_render.rs @@ -78,7 +78,7 @@ pub fn extract( || z > max_z + render_distance; if !deep_inside && !far_outside { let alpha = 1.0 - border.distance(x, z) / render_distance; - state.alpha = jclamp(alpha.powf(4.0), 0.0, 1.0); + state.alpha = jclamp(horizon_core::jmath::pow(alpha, 4.0), 0.0, 1.0); state.tint = status_color(border.status()); } else { state.alpha = 0.0; diff --git a/crates/horizon-client/src/game/net_harness.rs b/crates/horizon-client/src/game/net_harness.rs index 755a4c562..d9bb6a74b 100644 --- a/crates/horizon-client/src/game/net_harness.rs +++ b/crates/horizon-client/src/game/net_harness.rs @@ -21,6 +21,7 @@ impl Game<'_> { let mut quiet_since = Instant::now(); let mut step = 0; let mut step_at = Instant::now(); + let mut chat_at = 0; let mut position = None; let mut chunks = 0; let mut error = None; @@ -44,6 +45,10 @@ impl Game<'_> { last_chunks = count; quiet_since = now; } + // Each step waits for what it sent to be answered (its chat line, + // the /list reply), however busy the machine, up to 15 s. + let shown = self.remote.as_ref().map_or(0, |r| r.chat.len()); + let answered = shown > chat_at || now - step_at > Duration::from_secs(15); match step { 0 if spawned_at.is_some() && count > 0 @@ -53,13 +58,15 @@ impl Game<'_> { self.send_chat("hello from the horizon client"); step = 1; step_at = now; + chat_at = shown; } - 1 if now - step_at > Duration::from_millis(1500) => { + 1 if answered => { self.send_chat("/list"); step = 2; step_at = now; + chat_at = shown; } - 2 if now - step_at > Duration::from_millis(1500) => { + 2 if answered => { step = 3; break; } diff --git a/crates/horizon-client/src/mobs/animal_brain.rs b/crates/horizon-client/src/mobs/animal_brain.rs index 9edf2accf..cf7862773 100644 --- a/crates/horizon-client/src/mobs/animal_brain.rs +++ b/crates/horizon-client/src/mobs/animal_brain.rs @@ -742,7 +742,7 @@ impl Gate { // current order, then a stable sort. for k in &mut self.kids { let r = self.random.next_float(); - k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32)); + k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32)); } self.kids.sort_by(|a, b| a.2.total_cmp(&b.2)); for k in &mut self.kids { diff --git a/crates/horizon-client/src/mobs/breeze.rs b/crates/horizon-client/src/mobs/breeze.rs index 4a8484835..5d78fa8ab 100644 --- a/crates/horizon-client/src/mobs/breeze.rs +++ b/crates/horizon-client/src/mobs/breeze.rs @@ -6,7 +6,7 @@ use super::{ anim::{Poses, State}, keyframes::Animation, }; -use horizon_core::{geometry::Vec3, random::RandomSource}; +use horizon_core::{geometry::Vec3, jmath, random::RandomSource}; use std::collections::HashMap; // `variant3` is the pose, `variant` the pose the animations last saw, @@ -96,9 +96,9 @@ fn jump_vector(m: &Mob, target: Vec3, max_velocity: f32, angle: i32) -> Option Option max || zt.abs() > max { return None; } @@ -934,7 +934,7 @@ impl Goal for RandomLookAround { } fn start(&mut self, m: &mut Mob, _: &mut Cx) { let r = std::f64::consts::TAU * m.random.next_double(); - self.rel = [r.cos(), r.sin()]; + self.rel = [jmath::cos(r), jmath::sin(r)]; self.time = 20 + m.random.next_int_bound(20).unwrap_or(0); } fn every_tick(&self) -> bool { diff --git a/crates/horizon-client/src/mobs/monster_brain.rs b/crates/horizon-client/src/mobs/monster_brain.rs index c770858a6..bc2cfe705 100644 --- a/crates/horizon-client/src/mobs/monster_brain.rs +++ b/crates/horizon-client/src/mobs/monster_brain.rs @@ -550,7 +550,7 @@ impl RunOne { // order, then a stable sort. for k in &mut self.kids { let r = self.random.next_float(); - k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32)); + k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32)); } self.kids.sort_by(|a, b| a.2.total_cmp(&b.2)); for k in &mut self.kids { diff --git a/crates/horizon-client/src/mobs/villager_brain.rs b/crates/horizon-client/src/mobs/villager_brain.rs index 59ce7d2be..0ea449fab 100644 --- a/crates/horizon-client/src/mobs/villager_brain.rs +++ b/crates/horizon-client/src/mobs/villager_brain.rs @@ -1105,7 +1105,7 @@ impl Gate { // current order, then a stable sort. for k in &mut self.kids { let r = self.random.next_float(); - k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32)); + k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32)); } self.kids.sort_by(|a, b| a.2.total_cmp(&b.2)); } @@ -2396,7 +2396,7 @@ impl B { } => { for k in kids.iter_mut() { let r = random.next_float(); - k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32)); + k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32)); } kids.sort_by(|a, b| a.2.total_cmp(&b.2)); for k in kids.iter_mut() { diff --git a/crates/horizon-client/src/mobs/warden_brain.rs b/crates/horizon-client/src/mobs/warden_brain.rs index a79fe1477..41353fc83 100644 --- a/crates/horizon-client/src/mobs/warden_brain.rs +++ b/crates/horizon-client/src/mobs/warden_brain.rs @@ -844,7 +844,7 @@ impl B { // current order, then a stable sort. for k in &mut g.kids { let r = g.random.next_float(); - k.2 = -f64::from(r).powf(f64::from(1.0_f32 / k.1 as f32)); + k.2 = -horizon_core::jmath::pow(f64::from(r), f64::from(1.0_f32 / k.1 as f32)); } g.kids.sort_by(|a, b| a.2.total_cmp(&b.2)); for k in &mut g.kids { diff --git a/crates/horizon-client/src/options_file.rs b/crates/horizon-client/src/options_file.rs index fd606fd62..5c14450ec 100644 --- a/crates/horizon-client/src/options_file.rs +++ b/crates/horizon-client/src/options_file.rs @@ -298,7 +298,7 @@ fn double_range(v: f64, lo: f64, hi: f64) -> Option { (v.total_cmp(&lo).is_ge() && v.total_cmp(&hi).is_le() && !v.is_nan()).then_some(v) } fn log_mouse(value: i32) -> f64 { - 10f64.powf(f64::from(value) / 100.0) + horizon_core::jmath::pow(10.0, f64::from(value) / 100.0) } fn unit_alt(json: &Json) -> Option { codec_double(json) @@ -366,7 +366,7 @@ impl Spec { } (Spec::Fov, V::Int(x)) => (30..=110).contains(x).then(|| v.clone()), (Spec::WheelSensitivity, V::Double(x)) => { - let i = floor_int(x.log10() * 100.0); + let i = floor_int(horizon_core::jmath::log10(*x) * 100.0); (-200..=100).contains(&i).then(|| V::Double(log_mouse(i))) } (Spec::Unit, V::Double(x)) => (*x >= 0.0 && *x <= 1.0).then(|| v.clone()), diff --git a/crates/horizon-core/Cargo.toml b/crates/horizon-core/Cargo.toml index 655472a35..97c9356b8 100644 --- a/crates/horizon-core/Cargo.toml +++ b/crates/horizon-core/Cargo.toml @@ -10,6 +10,9 @@ description = "Horizon simulation foundations for the pinned vanilla Java Editio # Records the call sites of LegacyRandom draws (mob AI parity debugging). rng-trace = [] +[dependencies] +libm = "=0.2.16" + [[bin]] name = "horizon-parity" path = "src/bin/parity.rs" diff --git a/crates/horizon-core/src/density/arithmetic.rs b/crates/horizon-core/src/density/arithmetic.rs index 296a071e8..f82b96ce2 100644 --- a/crates/horizon-core/src/density/arithmetic.rs +++ b/crates/horizon-core/src/density/arithmetic.rs @@ -8,7 +8,7 @@ pub(super) fn java_power(base: f64, exponent: f64) -> f64 { } else if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) { f64::NAN } else { - base.powf(exponent) + crate::jmath::pow(base, exponent) } } @@ -83,7 +83,7 @@ impl UnaryOp { Self::Square => value * value, Self::Cube => value * value * value, Self::Sqrt => (value as f64).sqrt() as f32, - Self::Log => (value as f64).ln() as f32, + Self::Log => crate::jmath::log(value as f64) as f32, Self::PowerBase(base) => java_power(base, value as f64) as f32, Self::PowerExponent(exponent) => java_power(value as f64, exponent) as f32, Self::Reciprocal => 1.0 / value, diff --git a/crates/horizon-core/src/density/beardifier.rs b/crates/horizon-core/src/density/beardifier.rs index 225f74714..73b94a066 100644 --- a/crates/horizon-core/src/density/beardifier.rs +++ b/crates/horizon-core/src/density/beardifier.rs @@ -208,7 +208,7 @@ fn kernel() -> &'static [f32; 13824] { let x = ((i / 24) % 24) as f64 - 12.; let y = (i % 24) as f64 - 12. + 0.5; let squared = (x * x + y * y) + z * z; - std::f64::consts::E.powf(-squared / 16.) as f32 + crate::jmath::pow(std::f64::consts::E, -squared / 16.) as f32 }) }) } diff --git a/crates/horizon-core/src/density/interval.rs b/crates/horizon-core/src/density/interval.rs index 0375dba6d..0e2652fe1 100644 --- a/crates/horizon-core/src/density/interval.rs +++ b/crates/horizon-core/src/density/interval.rs @@ -189,7 +189,7 @@ impl Interval { Ok(Self::NAI) } else { self.maximum(Self::exact(0.0)?) - .map_monotonic(|v| (v as f64).ln() as f32) + .map_monotonic(|v| crate::jmath::log(v as f64) as f32) } } pub fn power(self, exponent: Self) -> Self { diff --git a/crates/horizon-core/src/jmath.rs b/crates/horizon-core/src/jmath.rs new file mode 100644 index 000000000..57775c674 --- /dev/null +++ b/crates/horizon-core/src/jmath.rs @@ -0,0 +1,617 @@ +//! `java.lang.Math` as HotSpot computes it on the machine Horizon runs on. +//! +//! Rust's float functions call the platform's libm, and those round +//! differently: glibc, Apple's libm and fdlibm disagree in the last bit for +//! a few percent of arguments. Vanilla's own results differ between machines +//! too, because HotSpot replaces some `Math` methods with intrinsics: +//! +//! - `atan2`, `atan`, `asin`, `acos` and `hypot` are `StrictMath`'s (fdlibm) +//! everywhere. +//! - On x86-64, `sin`, `cos`, `tan`, `exp`, `log`, `log10` and `pow` are +//! Intel's libm stubs, which round like glibc for nearly every argument; +//! there they use the platform libm, as before. +//! - On AArch64 only `sin` and `cos` are intrinsics: fdlibm's kernels +//! evaluated with fused multiply-adds (`macroAssembler_aarch64_trig.cpp`). +//! Everything else is `StrictMath`. +//! +//! So these functions return what vanilla returns on the same machine, on +//! Linux and on an Apple silicon Mac alike. + +/// `Math.sin`. +pub fn sin(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + aarch64::sin(x) + } else { + x.sin() + } +} + +/// `Math.cos`. +pub fn cos(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + aarch64::cos(x) + } else { + x.cos() + } +} + +/// `Math.tan`. +pub fn tan(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + libm::tan(x) + } else { + x.tan() + } +} + +/// `Math.exp`. +pub fn exp(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + libm::exp(x) + } else { + x.exp() + } +} + +/// `Math.log`. +pub fn log(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + fdlibm::log(x) + } else { + x.ln() + } +} + +/// `Math.log10`. +pub fn log10(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + fdlibm::log10(x) + } else { + x.log10() + } +} + +/// `Math.pow`. +pub fn pow(x: f64, y: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + libm::pow(x, y) + } else { + x.powf(y) + } +} + +/// `Math.cbrt`. +pub fn cbrt(x: f64) -> f64 { + if cfg!(target_arch = "aarch64") { + fdlibm::cbrt(x) + } else { + x.cbrt() + } +} + +/// `Math.atan2`. +pub fn atan2(y: f64, x: f64) -> f64 { + libm::atan2(y, x) +} + +/// `Math.atan`. +pub fn atan(x: f64) -> f64 { + libm::atan(x) +} + +/// `Math.asin`. +pub fn asin(x: f64) -> f64 { + libm::asin(x) +} + +/// `Math.acos`. +pub fn acos(x: f64) -> f64 { + libm::acos(x) +} + +/// `Math.hypot`. +pub fn hypot(x: f64, y: f64) -> f64 { + fdlibm::hypot(x, y) +} + +fn hi(x: f64) -> i32 { + (x.to_bits() >> 32) as i32 +} + +fn with_hi(x: f64, high: i32) -> f64 { + f64::from_bits((x.to_bits() & 0xffff_ffff) | (u64::from(high as u32) << 32)) +} + +/// `java.lang.FdLibm` ports (`StrictMath`) of what the `libm` crate computes +/// differently. +// fdlibm's constants and NaN, verbatim. +#[allow(clippy::excessive_precision, clippy::approx_constant, clippy::eq_op)] +pub mod fdlibm { + use super::{hi, with_hi}; + + const TWO54: f64 = 1.80143985094819840000e+16; + + /// `FdLibm.Log`. + pub fn log(mut x: f64) -> f64 { + const LN2_HI: f64 = 6.93147180369123816490e-01; + const LN2_LO: f64 = 1.90821492927058770002e-10; + const LG1: f64 = 6.666666666666735130e-01; + const LG2: f64 = 3.999999999940941908e-01; + const LG3: f64 = 2.857142874366239149e-01; + const LG4: f64 = 2.222219843214978396e-01; + const LG5: f64 = 1.818357216161805012e-01; + const LG6: f64 = 1.531383769920937332e-01; + const LG7: f64 = 1.479819860511658591e-01; + let mut hx = hi(x); + let lx = x.to_bits() as u32; + let mut k = 0; + if hx < 0x0010_0000 { + if ((hx & 0x7fff_ffff) as u32 | lx) == 0 { + return -TWO54 / 0.0; + } + if hx < 0 { + return (x - x) / 0.0; + } + k -= 54; + x *= TWO54; + hx = hi(x); + } + if hx >= 0x7ff0_0000 { + return x + x; + } + k += (hx >> 20) - 1023; + hx &= 0x000f_ffff; + let i = (hx + 0x9_5f64) & 0x10_0000; + x = with_hi(x, hx | (i ^ 0x3ff0_0000)); + k += i >> 20; + let f = x - 1.0; + if (0x000f_ffff & (2 + hx)) < 3 { + if f == 0.0 { + if k == 0 { + return 0.0; + } + let dk = f64::from(k); + return dk * LN2_HI + dk * LN2_LO; + } + let r = f * f * (0.5 - 0.33333333333333333 * f); + if k == 0 { + return f - r; + } + let dk = f64::from(k); + return dk * LN2_HI - ((r - dk * LN2_LO) - f); + } + let s = f / (2.0 + f); + let dk = f64::from(k); + let z = s * s; + let mut i = hx - 0x6_147a; + let w = z * z; + let j = 0x6b851 - hx; + let t1 = w * (LG2 + w * (LG4 + w * LG6)); + let t2 = z * (LG1 + w * (LG3 + w * (LG5 + w * LG7))); + i |= j; + let r = t2 + t1; + if i > 0 { + let hfsq = 0.5 * f * f; + if k == 0 { + f - (hfsq - s * (hfsq + r)) + } else { + dk * LN2_HI - ((hfsq - (s * (hfsq + r) + dk * LN2_LO)) - f) + } + } else if k == 0 { + f - s * (f - r) + } else { + dk * LN2_HI - ((s * (f - r) - dk * LN2_LO) - f) + } + } + + /// `FdLibm.Log10`. + pub fn log10(mut x: f64) -> f64 { + const IVLN10: f64 = 4.34294481903251816668e-01; + const LOG10_2HI: f64 = 3.01029995663611771306e-01; + const LOG10_2LO: f64 = 3.69423907715893078616e-13; + let mut hx = hi(x); + let lx = x.to_bits() as u32; + let mut k = 0; + if hx < 0x0010_0000 { + if ((hx & 0x7fff_ffff) as u32 | lx) == 0 { + return -TWO54 / 0.0; + } + if hx < 0 { + return (x - x) / 0.0; + } + k -= 54; + x *= TWO54; + hx = hi(x); + } + if hx >= 0x7ff0_0000 { + return x + x; + } + k += (hx >> 20) - 1023; + let i = ((k as u32) >> 31) as i32; + hx = (hx & 0x000f_ffff) | ((0x3ff - i) << 20); + let y = f64::from(k + i); + x = with_hi(x, hx); + let z = y * LOG10_2LO + IVLN10 * log(x); + z + y * LOG10_2HI + } + + /// `FdLibm.Cbrt`. + pub fn cbrt(mut x: f64) -> f64 { + const B1: i32 = 715094163; + const B2: i32 = 696219795; + const C: f64 = 5.42857142857142815906e-01; + const D: f64 = -7.05306122448979611050e-01; + const E: f64 = 1.41428571428571436819e+00; + const F: f64 = 1.60714285714285720630e+00; + const G: f64 = 3.57142857142857150787e-01; + if x == 0.0 || !x.is_finite() { + return x; + } + let sign = if x < 0.0 { -1.0 } else { 1.0 }; + x = x.abs(); + let mut t = 0.0f64; + if x < f64::MIN_POSITIVE { + t = 18014398509481984.0; // 2^54 + t *= x; + t = with_hi(t, hi(t) / 3 + B2); + } else { + t = with_hi(t, hi(x) / 3 + B1); + } + let mut r = t * t / x; + let mut s = C + r * t; + t *= G + F / (s + E + D / s); + t = f64::from_bits(t.to_bits() & 0xffff_ffff_0000_0000); + t = with_hi(t, hi(t) + 1); + s = t * t; + r = x / s; + let w = t + t; + r = (r - t) / (w + r); + t += t * r; + sign * t + } + + /// `FdLibm.Hypot`. + pub fn hypot(x: f64, y: f64) -> f64 { + let mut a = x.abs(); + let mut b = y.abs(); + if !a.is_finite() || !b.is_finite() { + if a == f64::INFINITY || b == f64::INFINITY { + return f64::INFINITY; + } + return a + b; + } + if b > a { + std::mem::swap(&mut a, &mut b); + } + let mut ha = hi(a); + let mut hb = hi(b); + if ha - hb > 0x3c0_0000 { + return a + b; + } + let mut k = 0; + if a > f64::from_bits(0x5f30_0000_ffff_ffff) { + ha -= 0x2580_0000; + hb -= 0x2580_0000; + a *= f64::from_bits(0x1a70_0000_0000_0000); // 2^-600 + b *= f64::from_bits(0x1a70_0000_0000_0000); + k += 600; + } + if b < f64::from_bits(0x20b0_0000_0000_0000) { + // 2^-500 + if b < f64::MIN_POSITIVE { + if b == 0.0 { + return a; + } + let t1 = f64::from_bits(0x7fd0_0000_0000_0000); // 2^1022 + b *= t1; + a *= t1; + k -= 1022; + } else { + ha += 0x2580_0000; + hb += 0x2580_0000; + a *= f64::from_bits(0x6570_0000_0000_0000); // 2^600 + b *= f64::from_bits(0x6570_0000_0000_0000); + k -= 600; + } + } + let mut w = a - b; + if w > b { + let t1 = with_hi(0.0, ha); + let t2 = a - t1; + w = (t1 * t1 - (b * (-b) - t2 * (a + t1))).sqrt(); + } else { + a += a; + let y1 = with_hi(0.0, hb); + let y2 = b - y1; + let t1 = with_hi(0.0, ha + 0x0010_0000); + let t2 = a - t1; + w = (t1 * y1 - (w * (-w) - (t1 * y2 + t2 * b))).sqrt(); + } + if k != 0 { + f64::from_bits(((1023 + k) as u64) << 52) * w + } else { + w + } + } +} + +/// HotSpot's AArch64 `Math.sin` / `Math.cos` stubs +/// (`MacroAssembler::generate_dsin_dcos`): fdlibm with fused multiply-adds. +/// Exposed on every architecture so they can be tested anywhere. +// HotSpot's tables, verbatim. +#[allow( + clippy::excessive_precision, + clippy::approx_constant, + clippy::unreadable_literal +)] +pub mod aarch64 { + use super::hi; + + const S: [f64; 6] = [ + -1.66666666666666324348e-01, + 8.33333333332248946124e-03, + -1.98412698298579493134e-04, + 2.75573137070700676789e-06, + -2.50507602534068634195e-08, + 1.58969099521155010221e-10, + ]; + const C: [f64; 6] = [ + 4.16666666666666019037e-02, + -1.38888888888741095749e-03, + 2.48015872894767294178e-05, + -2.75573143513906633035e-07, + 2.08757232129817482790e-09, + -1.13596475577881948265e-11, + ]; + const INVPIO2: f64 = 6.36619772367581382433e-01; + const PIO2_1: f64 = 1.57079632673412561417e+00; + const PIO2_1T: f64 = 6.07710050650619224932e-11; + const PIO2_2: f64 = 6.07710050630396597660e-11; + const PIO2_2T: f64 = 2.02226624879595063154e-21; + const PIO2_3: f64 = 2.02226624871116645580e-21; + const PIO2_3T: f64 = 8.47842766036889956997e-32; + const NPIO2_HW: [i32; 32] = [ + 0x3FF921FB, 0x400921FB, 0x4012D97C, 0x401921FB, 0x401F6A7A, 0x4022D97C, 0x4025FDBB, + 0x402921FB, 0x402C463A, 0x402F6A7A, 0x4031475C, 0x4032D97C, 0x40346B9C, 0x4035FDBB, + 0x40378FDB, 0x403921FB, 0x403AB41B, 0x403C463A, 0x403DD85A, 0x403F6A7A, 0x40407E4C, + 0x4041475C, 0x4042106C, 0x4042D97C, 0x4043A28C, 0x40446B9C, 0x404534AC, 0x4045FDBB, + 0x4046C6CB, 0x40478FDB, 0x404858EB, 0x404921FB, + ]; + + /// `generate_kernel_sin`. + fn kernel_sin(x: f64, y: f64, iy: bool) -> f64 { + let z = x * x; + let v = z * x; + let mut r = z.mul_add(S[5], S[4]); + r = z.mul_add(r, S[3]); + r = z.mul_add(r, S[2]); + r = z.mul_add(r, S[1]); + if !iy { + v.mul_add(z.mul_add(r, S[0]), x) + } else { + let t = (-v).mul_add(r, 0.5 * y); + let t = (-z).mul_add(t, y); + x + v.mul_add(S[0], t) + } + } + + /// `generate_kernel_cos`. `ix` is the high word of the *original* + /// argument: the stub reuses the register, so a reduced argument always + /// takes the `qx = 0.28125` branch. + fn kernel_cos(x: f64, y: f64, ix: i32) -> f64 { + let z = x * x; + let mut r = z.mul_add(C[5], C[4]); + r = z.mul_add(r, C[3]); + let xy = x * y; + r = z.mul_add(r, C[2]); + r = z.mul_add(r, C[1]); + let zz = z * z; + r = z.mul_add(r, C[0]); + if ix <= 0x3FD33333 { + let v = (-zz).mul_add(r, xy); + 1.0 - 0.5f64.mul_add(z, v) + } else { + let qx = if ix > 0x3fe90000 { + 0.28125 + } else { + f64::from_bits(u64::from((ix - 0x0020_0000) as u32) << 32) + }; + let zr_xy = zz.mul_add(r, -xy); + let h = 0.5f64.mul_add(z, -qx); + let a = 1.0 - qx; + a - (h - zr_xy) + } + } + + /// `generate__ieee754_rem_pio2` for |x| <= 2^19·π/2; `None` beyond. + fn rem_pio2(x: f64, ix: i32) -> Option<(i32, f64, f64)> { + if ix < 0x4002d97c { + return Some(if x.to_bits() as i64 >= 0 { + let z = x - PIO2_1; + if ix != 0x3ff921fb { + let y0 = z - PIO2_1T; + (1, y0, (z - y0) - PIO2_1T) + } else { + let z = z - PIO2_2; + let y0 = z - PIO2_2T; + (1, y0, (z - y0) - PIO2_2T) + } + } else { + let z = x + PIO2_1; + if ix != 0x3ff921fb { + let y0 = z + PIO2_1T; + (-1, y0, (z - y0) + PIO2_1T) + } else { + let z = z + PIO2_2; + let y0 = z + PIO2_2T; + (-1, y0, (z - y0) + PIO2_2T) + } + }); + } + if ix > 0x413921fb { + return None; + } + let t = x.abs(); + let fnv = t.mul_add(INVPIO2, 0.5).trunc(); + let n = fnv as i32; + let mut r = (-fnv).mul_add(PIO2_1, t); + let mut w = fnv * PIO2_1T; + let mut y0 = r - w; + if n > 32 || ix == NPIO2_HW[(n - 1) as usize] { + let j = ix >> 20; + let exponent = |y: f64| ((y.to_bits() << 1) >> 53) as i32; + if j - exponent(y0) > 16 { + let t = r; + let w2 = fnv * PIO2_2; + r = t - w2; + w = fnv.mul_add(PIO2_2T, w2 - (t - r)); + y0 = r - w; + if j - exponent(y0) > 49 { + let t = r; + let w3 = fnv * PIO2_3; + r = t - w3; + w = fnv.mul_add(PIO2_3T, w3 - (t - r)); + y0 = r - w; + } + } + } + let y1 = (r - y0) - w; + Some(if x > 0.0 { (n, y0, y1) } else { (-n, -y0, -y1) }) + } + + fn sin_cos(x: f64, is_cos: bool) -> f64 { + let ix = hi(x) & 0x7fff_ffff; + if ix < 0x3e400000 { + return if is_cos { 1.0 } else { x }; + } + if ix <= 0x3fe921fb { + return if is_cos { + kernel_cos(x, 0.0, ix) + } else { + kernel_sin(x, 0.0, false) + }; + } + if ix >= 0x7ff00000 { + return f64::from_bits(0x7ff0_0000_0000_0001); + } + // Beyond 2^19·π/2 the stub's Payne–Hanek reduction isn't ported: + // StrictMath's is used instead. + let Some((n, y0, y1)) = rem_pio2(x, ix) else { + return if is_cos { libm::cos(x) } else { libm::sin(x) }; + }; + let (value, negate) = if (n & 1 == 1) != is_cos { + ( + kernel_cos(y0, y1, ix), + if is_cos { (n ^ (n >> 1)) & 1 } else { n & 2 }, + ) + } else { + ( + kernel_sin(y0, y1, true), + if is_cos { (n ^ (n >> 1)) & 1 } else { n & 2 }, + ) + }; + if negate != 0 { -value } else { value } + } + + /// `Math.sin` on AArch64 HotSpot. + pub fn sin(x: f64) -> f64 { + sin_cos(x, false) + } + + /// `Math.cos` on AArch64 HotSpot. + pub fn cos(x: f64) -> f64 { + sin_cos(x, true) + } +} + +#[cfg(test)] +mod tests { + use super::{aarch64, fdlibm}; + + fn check1(f: fn(f64) -> f64, (x, expected): (u64, u64)) { + assert_eq!(f(f64::from_bits(x)).to_bits(), expected, "{x:x}"); + } + + fn check2(f: fn(f64, f64) -> f64, (x, y, expected): (u64, u64, u64)) { + assert_eq!( + f(f64::from_bits(x), f64::from_bits(y)).to_bits(), + expected, + "{x:x} {y:x}" + ); + } + + #[test] + fn matches_hotspot_on_aarch64() { + // (argument bits, Math result bits) from HotSpot 25 on an Apple M4, each + // one where glibc rounds differently. + // sin + check1(aarch64::sin, (0x3fe63c5ab66b09f0, 0x3fe47d34215b54f7)); + check1(aarch64::sin, (0xc0569150fe105e6f, 0xbfe7bc19d65ab55c)); + check1(aarch64::sin, (0x4072c3f15b47b5d6, 0xbfef32be0f0a9124)); + check1(aarch64::sin, (0x3fee86c6ffce047e, 0x3fea1a47f46cbf28)); + // cos + check1(aarch64::cos, (0x3fbb10690eeadb00, 0x3fefd24375dbae28)); + check1(aarch64::cos, (0xc0496cc08a65245d, 0x3feab1a44a8022c4)); + check1(aarch64::cos, (0x40099262f89e73f8, 0xbfeff3aa148795ce)); + check1(aarch64::cos, (0x4001ce4e022387bc, 0xbfe37dd512be4d00)); + // log + check1(fdlibm::log, (0x40569150fe105e6f, 0x401202e102f27b78)); + check1(fdlibm::log, (0x3ff5dec70f518390, 0x3fd4009cda1f8856)); + check1(fdlibm::log, (0x3feb3d17325d493e, 0xbfc49ee8c4866f84)); + check1(fdlibm::log, (0x3fe735a40edf77ba, 0xbfd48e1816b18bf2)); + // log10 + check1(fdlibm::log10, (0x3ff5dec70f518390, 0x3fc15fb52aa5f051)); + check1(fdlibm::log10, (0x3feb3d17325d493e, 0xbfb1e933cae02f5f)); + check1(fdlibm::log10, (0x3fe735a40edf77ba, 0xbfc1da98cbb8974c)); + check1(fdlibm::log10, (0x3fe7febd9ca1bab0, 0xbfc000e8e3b0b56e)); + // cbrt + check1(fdlibm::cbrt, (0x401bb21f8568bd0c, 0x3ffe7ee30c7a56f9)); + check1(fdlibm::cbrt, (0x3fc59f3a45c8fb80, 0x3fe1b0791e0f8a74)); + check1(fdlibm::cbrt, (0xbfdd73cdc693ddab, 0xbfe8b4a6f43f1b73)); + check1(fdlibm::cbrt, (0xbfeb0715a8425e10, 0xbfee3f99498da6f6)); + // hypot + check2( + fdlibm::hypot, + (0xc0569150fe105e6f, 0xc0056b970bf3b41a, 0x405693db72f461a6), + ); + check2( + fdlibm::hypot, + (0x3fdc5999df9270f4, 0xc006c3ad990f72c0, 0x400709de26a1e40a), + ); + check2( + fdlibm::hypot, + (0x401bb21f8568bd0c, 0xbffeb93d1a2ef904, 0x401cbdbb623d20c3), + ); + check2( + fdlibm::hypot, + (0x4026d31c32ddb50b, 0xc00e7b2771d6867c, 0x40281027bb313aae), + ); + // pow + check2( + libm::pow, + (0x3fee04c313607ea6, 0xc00fd156138baf96, 0x3ff4a199ac2c8e9c), + ); + check2( + libm::pow, + (0x401bb21f8568bd0c, 0xbffeb93d1a2ef904, 0x3f98ecc16a60feee), + ); + check2( + libm::pow, + (0x407383ff427dadbd, 0x3ffdc5d178d00264, 0x40e56695c058d704), + ); + check2( + libm::pow, + (0x3fe0c95b66dd6030, 0xc00c9cfde259a3e6, 0x4024194449e9deca), + ); + // exp + check1(libm::exp, (0x3fdc5999df9270f4, 0x3ff8eacc53fdbf56)); + check1(libm::exp, (0x3fcffff502ecbd80, 0x3ff48b5c78bca894)); + check1(libm::exp, (0x3fc59f3a45c8fb80, 0x3ff2f1c6cb35728c)); + check1(libm::exp, (0x4056561bf10ba7f6, 0x47fdd21dc9013084)); + // tan + check1(libm::tan, (0x40023034f56881f0, 0xbff2e3f762ac9810)); + check1(libm::tan, (0xbfebf862877c88ec, 0xbff31f61e3e8539e)); + check1(libm::tan, (0xc00e37e234490564, 0xbfe79ca6eee66726)); + check1(libm::tan, (0xc075c085c04800b5, 0x3fea18b4a92fcf22)); + } +} diff --git a/crates/horizon-core/src/lib.rs b/crates/horizon-core/src/lib.rs index 79045910a..5b46ce2ef 100644 --- a/crates/horizon-core/src/lib.rs +++ b/crates/horizon-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod biome; pub mod carving; pub mod density; pub mod geometry; +pub mod jmath; pub mod noise; pub mod paletted; pub mod position; diff --git a/crates/horizon-core/src/random.rs b/crates/horizon-core/src/random.rs index 89de3e1f1..609cf59be 100644 --- a/crates/horizon-core/src/random.rs +++ b/crates/horizon-core/src/random.rs @@ -300,7 +300,7 @@ pub fn marsaglia_polar(cache: &mut Option, mut next_double: impl FnMut() -> if radius >= 1.0 || radius == 0.0 { continue; } - let multiplier = (-2.0 * radius.ln() / radius).sqrt(); + let multiplier = (-2.0 * crate::jmath::log(radius) / radius).sqrt(); *cache = Some(y * multiplier); return x * multiplier; } diff --git a/crates/horizon-world/src/level/redstone.rs b/crates/horizon-world/src/level/redstone.rs index 6e4722844..92f9059e5 100644 --- a/crates/horizon-world/src/level/redstone.rs +++ b/crates/horizon-world/src/level/redstone.rs @@ -1092,7 +1092,7 @@ impl Sim<'_, L> { let pitch = if mob_head { 1.0 } else { - 2f64.powf(f64::from(note - 12) / 12.0) as f32 + horizon_core::jmath::pow(2.0, f64::from(note - 12) / 12.0) as f32 }; let sound = match instrument.as_str() { "custom_head" => "minecraft:ui.button.click".to_owned(), diff --git a/crates/horizon-world/src/level/sculk.rs b/crates/horizon-world/src/level/sculk.rs index a831f2151..46073cf5e 100644 --- a/crates/horizon-world/src/level/sculk.rs +++ b/crates/horizon-world/src/level/sculk.rs @@ -315,7 +315,8 @@ impl Sim<'_, L> { if charge > 0 && let Some(faces) = faces { - let particles = ((f64::from(charge) + 1.0).ln() as f32 / 2.3) as i32 + 1; + let particles = + (horizon_core::jmath::log(f64::from(charge) + 1.0) as f32 / 2.3) as i32 + 1; let data = (particles << 6) + pack_faces(&faces); self.level_event(3006, pos, data); } diff --git a/crates/horizon-world/src/worldgen/dripstone.rs b/crates/horizon-world/src/worldgen/dripstone.rs index 2358bd723..8b67e0eca 100644 --- a/crates/horizon-world/src/worldgen/dripstone.rs +++ b/crates/horizon-world/src/worldgen/dripstone.rs @@ -545,9 +545,9 @@ fn speleothem_height(distance: f64, radius: f64, scale: f64, bluntness: f64) -> distance }; let r = distance / radius * 0.384; - let part1 = 0.75 * r.powf(1.3333333333333333); - let part2 = r.powf(0.6666666666666666); - let part3 = 0.3333333333333333 * r.ln(); + let part1 = 0.75 * horizon_core::jmath::pow(r, 1.3333333333333333); + let part2 = horizon_core::jmath::pow(r, 0.6666666666666666); + let part3 = 0.3333333333333333 * horizon_core::jmath::log(r); let height = (scale * (part1 - part2 - part3)).max(0.0); height / 0.384 * radius } diff --git a/crates/horizon-world/src/worldgen/end_spike.rs b/crates/horizon-world/src/worldgen/end_spike.rs index a2c00ee08..e76982853 100644 --- a/crates/horizon-world/src/worldgen/end_spike.rs +++ b/crates/horizon-world/src/worldgen/end_spike.rs @@ -38,8 +38,8 @@ pub fn spikes_for_seed(seed: i64) -> Vec { 2.0 * (-std::f64::consts::PI + (std::f64::consts::PI / 10.0) * f64::from(i)); let size = sizes[i as usize]; EndSpike { - center_x: (42.0 * angle.cos()).floor() as i32, - center_z: (42.0 * angle.sin()).floor() as i32, + center_x: (42.0 * horizon_core::jmath::cos(angle)).floor() as i32, + center_z: (42.0 * horizon_core::jmath::sin(angle)).floor() as i32, radius: 2 + size / 3, height: 76 + size * 3, guarded: size == 1 || size == 2, diff --git a/crates/horizon-world/src/worldgen/iceberg.rs b/crates/horizon-world/src/worldgen/iceberg.rs index 2bfeff29a..3a7409ad3 100644 --- a/crates/horizon-world/src/worldgen/iceberg.rs +++ b/crates/horizon-world/src/worldgen/iceberg.rs @@ -29,7 +29,7 @@ fn ceil(v: f32) -> i32 { f64::from(v).ceil() as i32 } fn pow2(v: i32) -> f64 { - f64::from(v).powf(2.0) + horizon_core::jmath::pow(f64::from(v), 2.0) } impl IcebergFeature { pub(crate) fn decode( @@ -380,7 +380,11 @@ impl IcebergFeature { fn signed_ellipse(xo: i32, zo: i32, origin: [i32; 3], a: i32, c: i32, angle: f64) -> f64 { let dx = f64::from(xo - origin[0]); let dz = f64::from(zo - origin[2]); - ((dx * angle.cos() - dz * angle.sin()) / f64::from(a)).powf(2.0) - + ((dx * angle.sin() + dz * angle.cos()) / f64::from(c)).powf(2.0) + let (sin, cos) = ( + horizon_core::jmath::sin(angle), + horizon_core::jmath::cos(angle), + ); + horizon_core::jmath::pow((dx * cos - dz * sin) / f64::from(a), 2.0) + + horizon_core::jmath::pow((dx * sin + dz * cos) / f64::from(c), 2.0) - 1.0 } diff --git a/crates/horizon-world/src/worldgen/ore_feature.rs b/crates/horizon-world/src/worldgen/ore_feature.rs index c4cbe447e..9d723ae40 100644 --- a/crates/horizon-world/src/worldgen/ore_feature.rs +++ b/crates/horizon-world/src/worldgen/ore_feature.rs @@ -155,7 +155,10 @@ impl OreFeature { let spread_xy = self.size as f32 / 8.0; let max_radius = ((self.size as f32 / 16.0 * 2.0 + 1.0) / 2.0).ceil() as i32; let (ox, oz) = (f64::from(origin[0]), f64::from(origin[2])); - let (sin, cos) = (f64::from(dir).sin(), f64::from(dir).cos()); + let (sin, cos) = ( + horizon_core::jmath::sin(f64::from(dir)), + horizon_core::jmath::cos(f64::from(dir)), + ); let spread = f64::from(spread_xy); let x0 = ox + sin * spread; let x1 = ox - sin * spread; diff --git a/crates/horizon-world/src/worldgen/structure/stronghold.rs b/crates/horizon-world/src/worldgen/structure/stronghold.rs index 95fcd679c..eb31e0627 100644 --- a/crates/horizon-world/src/worldgen/structure/stronghold.rs +++ b/crates/horizon-world/src/worldgen/structure/stronghold.rs @@ -30,7 +30,10 @@ pub fn ring_positions( let dist = f64::from(4 * distance + distance * circle * 6) + (random.next_double() - 0.5) * (f64::from(distance) * 2.5); let round = |v: f64| (v + 0.5).floor() as i64 as i32; - let (x, z) = (round(angle.cos() * dist), round(angle.sin() * dist)); + let (x, z) = ( + round(horizon_core::jmath::cos(angle) * dist), + round(horizon_core::jmath::sin(angle) * dist), + ); let mut search = random.fork(); let (cx, cz) = ((x * 16 + 8) >> 2, (z * 16 + 8) >> 2); let mut found = 0; diff --git a/crates/horizon-world/src/worldgen/tree/trunk.rs b/crates/horizon-world/src/worldgen/tree/trunk.rs index b88e88bfc..741f0e2a7 100644 --- a/crates/horizon-world/src/worldgen/tree/trunk.rs +++ b/crates/horizon-world/src/worldgen/tree/trunk.rs @@ -690,8 +690,9 @@ impl TrunkPlacer { let height = tree_height + 2; let trunk_height = (f64::from(height) * 0.618).floor() as i32; Self::place_below_trunk(run, tree, tb::offset(origin, 0, -1, 0))?; - let clusters_per_y = - 1.min((1.382 + (1.0 * f64::from(height) / 13.0).powf(2.0)).floor() as i32); + let clusters_per_y = 1.min( + (1.382 + horizon_core::jmath::pow(1.0 * f64::from(height) / 13.0, 2.0)).floor() as i32, + ); let trunk_top = origin[1].wrapping_add(trunk_height); let mut relative_y = height - 5; // (attachment position, branch base y) @@ -704,8 +705,8 @@ impl TrunkPlacer { let radius = 1.0 * f64::from(shape) * (f64::from(run.random.next_float()) + 0.328); let angle = f64::from(run.random.next_float() * 2.0f32) * std::f64::consts::PI; - let x = radius * angle.sin() + 0.5; - let z = radius * angle.cos() + 0.5; + let x = radius * horizon_core::jmath::sin(angle) + 0.5; + let z = radius * horizon_core::jmath::cos(angle) + 0.5; let check_start = tb::offset(origin, x.floor() as i32, relative_y - 1, z.floor() as i32); let check_end = tb::offset(check_start, 0, 5, 0); diff --git a/docs/parity-workflow.md b/docs/parity-workflow.md index d84b46acb..fc35c9e61 100644 --- a/docs/parity-workflow.md +++ b/docs/parity-workflow.md @@ -32,7 +32,7 @@ Every runner on the machine shares `PARITY_MACHINE_JOBS` slots (default: half th Harnesses can also run on the Mac mini and the ThinkPad (`tools/parity_hosts.json` describes them; its `default` list is empty, so pass `--hosts macmini,thinkpad` or set `PARITY_HOSTS`, after copying `shared-local/` to `~/code/horizon-parity/shared-local` on each host). The runner mirrors the worktree there with rsync (not `.git`, `.local`, `target` or ignored files) into `~/code/horizon-parity/remote/`, whose `.local` links to the vanilla data in `~/code/horizon-parity/shared-local`, and starts `tools/parity.py --worker` over ssh. The worker builds the probes (about 80 s the first time) and runs what it is sent under the same tracer, admitting harnesses against its own free memory. One queue, longest first, feeds every machine, remote ones first: the Mac takes up to 14 harnesses, the desktop 3 and only those under 800 MB. Each pass comes back with the content hash of every file it depended on, so it is keyed on exactly what ran, and logs come back to `.local/parity-runner/logs/`; state, sharing and `gate.py` work as before. If a host can't be reached, or drops out, its harnesses run here. -Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. On macOS, `options` and `chunk_generation` differ because Rust's float functions (`f64::powf` and the like) use the platform libm, which rounds differently from glibc; every other harness gave the same result on the Mac as on Linux. The ThinkPad helps too, gently: Linux, 2 harnesses at `nice 19`, since it hosts the Claude account pool; it finds ALSA through `~/code/horizon-parity/alsa-dev` like the desktop. +Harnesses a host can't run are listed in the file: `local_only` (`save_roundtrip` runs the game on a virtual screen) and each host's `exclude`. The Mac excludes nothing: vanilla's `Math.sin`, `pow`, `log` and the like don't round the same on an Apple silicon Mac as on Linux (HotSpot uses fdlibm there, Intel's stubs on x86-64), and Rust's float functions round like neither (they call the platform libm), so code that mirrors a vanilla `Math` call uses `horizon_core::jmath`, which computes what HotSpot computes on the host architecture. The ThinkPad helps too, gently: Linux, 2 harnesses at `nice 19`, since it hosts the Claude account pool; it finds ALSA through `~/code/horizon-parity/alsa-dev` like the desktop. ### Shards and Rust scopes diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaProtocolClient.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaProtocolClient.java index 4a0cde956..d03cab7d3 100644 --- a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaProtocolClient.java +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaProtocolClient.java @@ -478,9 +478,12 @@ public final class VanillaProtocolClient { record("play", r.type + " " + json(p.reason())); break; } - // The clock goes out every second: after the first, where it - // falls among the joining packets is timing. - if (ordered && !(r.type.equals("minecraft:set_time") && counts.get(r.type) > 1)) { + // MinecraftServer.tickServer syncs the game time every 20 ticks + // (no clocks): whether one lands before the first chunk batch + // depends on when the client joined, so it isn't compared. + boolean periodicTime = packet instanceof net.minecraft.network.protocol.game.ClientboundSetTimePacket p + && p.clockUpdates().isEmpty(); + if (ordered && !periodicTime) { record("play-start", r.type + " " + summary(r)); } if (packet instanceof net.minecraft.network.protocol.game.ClientboundCommandsPacket p && counts.get(r.type) == 1) { diff --git a/tools/parity_hosts.json b/tools/parity_hosts.json index 5347c9257..4d5bd1bcd 100644 --- a/tools/parity_hosts.json +++ b/tools/parity_hosts.json @@ -9,12 +9,7 @@ "python": "/opt/homebrew/bin/python3.13", "rsync": "/opt/homebrew/bin/rsync", "jobs": 14, - "exclude": { - "options": "Rust's f64::powf uses the platform libm, which rounds differently on macOS", - "chunk_generation": "one ocean ruined portal differs on macOS (platform float math)", - "world_border_render": "the wall's alpha differs by one ulp on macOS (platform float math)", - "mob_ai": "mob movement differs on macOS (platform float math in random positions)" - } + "exclude": {} }, "thinkpad": { "ssh": "thinkpad", diff --git a/tools/test_dedicated_server_parity.py b/tools/test_dedicated_server_parity.py index d3c9602a9..3dbbbc7c6 100644 --- a/tools/test_dedicated_server_parity.py +++ b/tools/test_dedicated_server_parity.py @@ -9,9 +9,9 @@ headless client made of vanilla's own codecs (any byte vanilla would not read is an error), pings each server, joins it in offline mode through login, configuration and play, accepts the spawn teleport, receives the chunks around the spawn (their bytes are compared: the world is loaded from -the same files), chats, runs /list, then commands that fail (`/random value -7..7`, `/random value x`, `/trigger nope`) and leaves. The console then runs `list` -and `say` and `stop`. Compared, all of them exactly: +the same files), chats, runs /list and commands that fail (`/random value +7..7`, `/random value x`, `/trigger nope`) and leaves. The console then +runs `list` and `say` and `stop`. Compared, all of them exactly: - the status response (MOTD, players, version, favicon) and the ping; - the legacy (pre-1.7, 0xFE) ping reply; @@ -143,6 +143,36 @@ class Server: self.log.close() +def settle(server, ticks=600, timeout=600): + """Waits until every force-loaded chunk is loaded, then for `ticks` game + ticks, so their fluids have finished flowing however slow the machine.""" + deadline = time.time() + timeout + offsets = range(-80, 81, 16) + for poll in range(1_000_000): + start = len(server.lines) + for dx in offsets: + for dz in offsets: + server.console(f'execute unless loaded ~{dx} ~ ~{dz} run say unloaded') + server.console(f'say poll {poll}') + server.wait_for(rf'\[Server\] poll {poll}$', timeout=max(1, deadline - time.time())) + if not any(l.endswith('[Server] unloaded') for l in server.lines[start:]): + break + time.sleep(2) + + def game_time(): + start = len(server.lines) + server.console('time query gametime') + while time.time() < deadline: + for line in server.lines[start:]: + if m := re.search(r'The (?:game )?time is (\d+)', line): + return int(m.group(1)) + time.sleep(0.2) + raise AssertionError(f'{server.log_path.name}: no game time in {timeout} s') + until = game_time() + ticks + while game_time() < until: + time.sleep(2) + + def legacy_ping(port): """The reply to a 1.6 client's 0xFE 0x01 0xFA MC|PingHost query (LegacyQueryHandler).""" host = 'localhost' @@ -307,16 +337,17 @@ def main(): server.wait_for(r'Done \(') server.console('gamerule respawn_radius 0') server.wait_for(r'respawn_radius', timeout=30) - # No random ticks: which of the chunks around the player have grown - # kelp or spread grass by the time they are sent is timing. - server.console('gamerule random_tick_speed 0') - server.wait_for(r'random_tick_speed', timeout=30) # Vanilla keeps no spawn chunks: force-load the area the client is # sent and its neighbours (the console stands at the world spawn) so # both servers load full chunks instead of finishing proto-chunks. + # Random ticks change blocks while the client is being sent its + # chunks, more of them the busier the machine: none, so both servers + # send the world as it was saved. + server.console('gamerule random_tick_speed 0') + server.wait_for(r'random_tick_speed', timeout=30) server.console('forceload add ~-80 ~-80 ~80 ~80') server.wait_for(r'chunks in the Overworld are now force loaded|forceloaded|force loaded', timeout=60) - time.sleep(20) + settle(server) server.console('forceload remove all') server.console('save-all flush') server.wait_for(r'Saved the game', timeout=120) @@ -361,7 +392,8 @@ def main(): java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0], lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'), scope='Dedicated server seen by a vanilla-codec client: status, legacy ping, login, configuration, play ' - 'start, chunks, chat, /list, console list/say/stop, files written. Offline mode.')) + 'start (recipes, advancements, command tree), chunks, chat, /list, failing commands\' replies, ' + 'console list/say/stop, files written. Offline mode.')) print(f'dedicated server: {len(vanilla)} transcript lines identical')