Compare commits

..
Author SHA1 Message Date
rdbt 36eb98b6f6 Merge pull request #15815 from rdbtCVS/t3/fix-nightly-parity-regressions
Fix seven parity regressions blocking nightly builds
2026-11-12 23:53:35 +00:00
rdbtCVSandCodex GPT-6.1 a884525213 Align the head cache test with whole-payload texture rejection
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 23:23:18 +00:00
rdbtCVSandCodex GPT-6.1 c35f0ba323 Clarify the authlib reference outside the game port ledger
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 23:04:01 +00:00
rdbtCVSandCodex GPT-6.1 7e6b2db114 Merge main with the manual-proof parity fix
Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 22:55:36 +00:00
rdbtCVSandCodex GPT-6.1 768f0ddb83 Fix seven parity regressions blocking nightly builds
Restore vanilla void thresholds, HUD backdrops and profile texture validation; repair the block-event, AI-label, display-name and save-timer harness contracts.

Co-Authored-By: Codex GPT-6.1 <[email protected]>
2026-11-12 22:55:15 +00:00
rdbt 7c1a43138c Merge pull request #15570 from ilguerro/fix/parity-manual-proof-scripts
Leave the six hand-run proof scripts out of parity runs
2026-11-12 22:43:08 +00:00
Riccardo GuerrieroandClaude Opus 5.5 cc1fea2714 Leave the six hand-run proof scripts out of parity runs
tools/parity.py runs every tools/test_*_parity.py with no arguments. Six of
them are two-step proofs that need arguments (a Java side, then a native
side against its reference): builtin_component_delivery,
entity_movement_predicate, guest_close_event, machine_recipe_placement,
predicate_reference_startup and self_pose_metadata. Run bare, each exits
in 0 s with a usage error, so every parity-main run since they landed
(11-07 to 11-09) has failed on them, and the Nightly waits for a green
parity-main.

A script can now declare PARITY_MANUAL = "<how to run it>": parity.py
leaves it out of every run (--changed and parity-main included) and,
asked for it by name, says how to run it. The six declare it; nothing
else about them changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd42tRMKJk8JighAmETj
2026-11-12 11:08:41 +01:00
24 changed files with 145 additions and 594 deletions

No files matched your search

@@ -39,14 +39,13 @@ pub fn text_with_backdrop(
background: u32,
) {
if background != 0 {
// Font.PreparedTextBuilder.markBackground: one pixel above and left of
// the glyphs, the glyph advance to the right and nine high: not a
// fixed two-pixel frame.
// GuiGraphicsExtractor.textWithBackdrop adds two pixels around the
// text's advance and the font's nine-pixel line height.
out.push(fill(
x - 1,
y - 1,
x + w,
y + 9,
x - 2,
y - 2,
x + w + 2,
y + 11,
multiply(background, color),
));
}
@@ -1491,10 +1490,10 @@ mod boss_bar_room_tests {
mod backdrop_tests {
use super::*;
/// `markBackground`: the backdrop reaches one pixel above and left of the
/// glyphs, their advance to the right and nine down, not 2 on every side.
/// `GuiGraphicsExtractor.textWithBackdrop`: two pixels around the advance
/// and the nine-pixel font line, independently of glyph-background drawing.
#[test]
fn the_backdrop_frames_the_glyphs_by_one_pixel() {
fn the_backdrop_frames_the_text_by_two_pixels() {
let mut out = Vec::new();
text_with_backdrop(
&mut out,
@@ -1508,6 +1507,6 @@ mod backdrop_tests {
let Some(GuiDraw::Fill { x0, y0, x1, y1, .. }) = out.first() else {
panic!("a backdrop is a fill first");
};
assert_eq!((*x0, *y0, *x1, *y1), (9, 19, 16, 29));
assert_eq!((*x0, *y0, *x1, *y1), (8, 18, 18, 31));
}
}
+11 -17
View File
@@ -93,18 +93,17 @@ pub fn unpack(value: &str, signature: Signature, valid_uris: &[impl AsRef<str>])
signed: signature == Signature::Signed,
..empty.clone()
};
// Each texture is loaded independently: an invalid or disallowed URL for one
// texture (cape, elytra) must not drop another (the skin). Vanilla's
// SkinManager works the same way.
// Authlib's MinecraftServicesSessionService validates every URL in
// unpackTextures before returning any textures. A bad cape rejects the skin too.
for (kind, texture) in textures {
let Some(url) = texture.get("url").and_then(|u| u.as_str()) else {
continue;
return empty;
};
let allowed = valid_uris
.iter()
.any(|u| url.starts_with(&u.as_ref().replace("{textureId}", "")));
if !allowed {
continue;
return empty;
}
if kind == "SKIN" {
out.slim = texture
@@ -1388,10 +1387,9 @@ mod tests {
&super::TEXTURE_URIS,
);
assert_eq!(textures.slim, slim && !url.contains("evil"));
// Textures are judged per URL (vanilla's SkinManager): the
// cape's URL is always allowed here, so it is kept even when
// the skin's URL is not.
assert!(textures.cape.is_some());
// Authlib rejects the whole payload if any URL is invalid,
// including the allowed cape when the skin is disallowed.
assert_eq!(textures.cape.is_some(), !url.contains("evil"));
assert_eq!(parsed.skin.is_some(), !url.contains("evil"));
assert!(std::sync::Arc::ptr_eq(&parsed, &cache.get(&profile)));
}
@@ -1544,18 +1542,14 @@ mod tests {
assert!(t.slim);
assert!(!unpack(wide, Signature::Signed, &TEXTURE_URIS).slim);
}
/// `unpack` keeps each texture independently: a cape from another origin
/// must not drop the skin.
/// Authlib's `MinecraftServicesSessionService` rejects the whole profile in
/// `unpackTextures` when any texture URL is outside the allowed origins.
#[test]
fn unpack_keeps_the_skin_when_another_texture_is_rejected() {
fn unpack_rejects_the_skin_when_another_texture_is_rejected() {
use super::{Signature, TEXTURE_URIS, unpack};
let mixed = "eyJ0ZXh0dXJlcyI6IHsiU0tJTiI6IHsidXJsIjogImh0dHBzOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL3NraW4xIn0sICJDQVBFIjogeyJ1cmwiOiAiaHR0cHM6Ly9ldmlsLmV4YW1wbGUvdGV4dHVyZS9jYXBlMSJ9fX0=";
let t = unpack(mixed, Signature::Unsigned, &TEXTURE_URIS);
assert_eq!(
t.skin.as_deref(),
Some("https://textures.minecraft.net/texture/skin1")
);
assert_eq!(t.cape, None);
assert_eq!(t, super::Textures::default());
}
#[test]
fn profiles_parse_like_resolvable_profiles() {
@@ -237,7 +237,7 @@ fn goal_name(m: &Mob, name: &str) -> String {
"RestrictSun" => "RestrictSunGoal",
"FleeSun" => "FleeSunGoal",
// `Cat.CatAvoidEntityGoal`, `Fox.FoxAvoidEntityGoal`... name their subclass.
"AvoidEntity" if m.species() == Species::Cat => "CatAvoidEntityGoal",
"AvoidEntity" | "CatAvoid" if m.species() == Species::Cat => "CatAvoidEntityGoal",
"AvoidEntity" => "AvoidEntityGoal",
"BowAttack" => "RangedBowAttackGoal",
"Swell" => "SwellGoal",
@@ -18,7 +18,7 @@ impl Game<'_> {
/// `Entity.checkBelowWorld` for the boat at `i`: true when it fell out of
/// the world and is gone.
pub(in crate::game) fn boat_below_world(&mut self, i: usize) -> bool {
if !below_world(self.boats[i].position.y, self.world.bottom_y()) {
if !below_world(self.boats[i].position.y, self.world.shape.min_y) {
return false;
}
self.eject_boat_mobs(i, true);
@@ -41,7 +41,7 @@ impl Game<'_> {
kind: &str,
) -> Option<usize> {
let i = cart_at(&self.carts, id, slot)?;
if !below_world(self.carts[i].position.y, self.world.bottom_y()) {
if !below_world(self.carts[i].position.y, self.world.shape.min_y) {
return Some(i);
}
self.eject_cart_mobs(i);
@@ -68,44 +68,51 @@ mod tests {
assert!(below_world(-64.5, 0));
}
/// A tick discards the boat and the chest minecart under -128 in the
/// overworld, before they move, and spills the cart's slots; the two on
/// the line or above it stay, whatever their fall this tick.
/// A tick discards vehicles below each dimension's own floor, even when
/// its chunk storage extends lower, and spills the container's contents.
/// Vehicles on the line or above it survive this tick.
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn a_tick_discards_the_vehicles_under_the_world() {
use horizon_client::chunk::Chunk;
use horizon_core::position::ChunkPos;
let mut g = crate::game::native_machines::tests::server_game();
g.build_registries();
g.set_allow_commands(true);
g.world
.chunks
.insert((0, 0), std::sync::Arc::new(Chunk::empty(0, 0)));
g.block_ticks.add_empty_chunk(ChunkPos::new(0, 0)).unwrap();
assert!(g.run_command("forceload add 0 0"));
for summon in [
"summon oak_boat 0.5 -128 0.5",
"summon oak_boat 4.5 -128.5 4.5",
"summon minecart 6.5 -127 6.5",
"summon chest_minecart 2.5 -129 2.5 {Items:[{Slot:0b,id:\"minecraft:diamond\",count:3}]}",
] {
assert!(g.run_command(summon), "{summon}");
for min_y in [-64, 0] {
let mut g = crate::game::native_machines::tests::server_game();
g.world.shape.min_y = min_y;
g.build_registries();
g.set_allow_commands(true);
g.world
.chunks
.insert((0, 0), std::sync::Arc::new(Chunk::empty(0, 0)));
g.block_ticks.add_empty_chunk(ChunkPos::new(0, 0)).unwrap();
assert!(g.run_command("forceload add 0 0"));
let line = min_y - 64;
for summon in [
format!("summon oak_boat 0.5 {line} 0.5"),
format!("summon oak_boat 4.5 {} 4.5", f64::from(line) - 0.5),
format!("summon minecart 6.5 {} 6.5", line + 1),
format!(
"summon chest_minecart 2.5 {} 2.5 {{Items:[{{Slot:0b,id:\"minecraft:diamond\",count:3}}]}}",
line - 1
),
] {
assert!(g.run_command(&summon), "{summon}");
}
g.sync_tick_chunks();
g.tick_boats();
g.tick_carts();
let boats: Vec<f64> = g.boats.iter().map(|b| b.position.x).collect();
assert_eq!(boats, [0.5]);
let carts: Vec<f64> = g.carts.iter().map(|c| c.position.x).collect();
assert_eq!(carts, [6.5]);
let spilled: u32 = g
.dropped
.iter()
.filter(|i| &*i.stack.item == "minecraft:diamond")
.map(|i| i.stack.count)
.sum();
assert_eq!(spilled, 3);
}
g.sync_tick_chunks();
g.tick_boats();
g.tick_carts();
let boats: Vec<f64> = g.boats.iter().map(|b| b.position.x).collect();
assert_eq!(boats, [0.5]);
let carts: Vec<f64> = g.carts.iter().map(|c| c.position.x).collect();
assert_eq!(carts, [6.5]);
let spilled: u32 = g
.dropped
.iter()
.filter(|i| &*i.stack.item == "minecraft:diamond")
.map(|i| i.stack.count)
.sum();
assert_eq!(spilled, 3);
}
#[test]
@@ -72,46 +72,6 @@ thread_local! {
static FAIL_TURN: Cell<Option<usize>> = const { Cell::new(None) };
}
/// TEMP-DIAG: what each lane did in a grouped tick, and what the server thread did around them.
#[cfg(test)]
pub(super) mod diag {
use std::sync::Mutex;
#[derive(Clone, Debug, Default)]
pub struct Lane {
pub server: bool,
pub mobs: usize,
pub busy_ns: u64,
pub cpu_ns: u64,
pub waited_ns: u64,
}
#[derive(Clone, Debug, Default)]
pub struct Tick {
pub setup_ns: u64,
pub threads_ns: u64,
pub join_wait_ns: u64,
pub merge_ns: u64,
pub groups: Vec<usize>,
pub lanes: Vec<Lane>,
}
pub static LAST: Mutex<Tick> = Mutex::new(Tick {
setup_ns: 0,
threads_ns: 0,
join_wait_ns: 0,
merge_ns: 0,
groups: Vec::new(),
lanes: Vec::new(),
});
/// The last plan's (ticking mobs, island weights heaviest first).
pub static ISLANDS: Mutex<(usize, Vec<usize>)> = Mutex::new((0, Vec::new()));
pub static PLAN_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn thread_cpu_ns() -> u64 {
std::fs::read_to_string("/proc/thread-self/schedstat")
.ok()
.and_then(|s| s.split_whitespace().next().and_then(|v| v.parse().ok()))
.unwrap_or(0)
}
}
#[cfg(test)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum FailAt {
@@ -223,17 +183,9 @@ impl Islands<'_> {
.chain(start.guests.iter().map(|g| &g.1))
.map(|p| (p.uuid, p.position))
.collect();
#[cfg(test)]
let began = std::time::Instant::now();
let planned = horizon_client::spans::time("islands_plan", || {
horizon_client::spans::time("islands_plan", || {
islands(&reaches(mobs, snapshot, turns, &players)?, lanes)
});
#[cfg(test)]
diag::PLAN_NS.store(
began.elapsed().as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
planned
})
}
/// The loop's turns in the plan's groups, each on its own thread, put back together as the
@@ -256,14 +208,6 @@ impl Islands<'_> {
turns.cached_difficulty = None;
(serial, seeds)
});
#[cfg(test)]
let setup_began = std::time::Instant::now();
#[cfg(test)]
{
let mut d = diag::LAST.lock().unwrap();
*d = diag::Tick::default();
d.groups = plan.groups.iter().map(Vec::len).collect();
}
let n = mobs.len();
let lanes = plan.groups.len();
let draws = OrderedDraws::new(&start.random, lanes);
@@ -308,10 +252,6 @@ impl Islands<'_> {
#[cfg(test)]
RAN.with(|r| r.set(lanes));
let mut board = Board::new(snapshot, &groups, Arc::clone(&draws));
#[cfg(test)]
{
diag::LAST.lock().unwrap().setup_ns = setup_began.elapsed().as_nanos() as u64;
}
self.run_threads(
&mut groups,
&board,
@@ -319,8 +259,6 @@ impl Islands<'_> {
shared,
serial.as_ref().map(|s| &s.1),
);
#[cfg(test)]
let merge_began = std::time::Instant::now();
// Each mob's entries, in list order: the turns' ranges in its group.
let mut place = vec![(0u32, 0u32); n];
for (gi, g) in groups.iter().enumerate() {
@@ -388,10 +326,6 @@ impl Islands<'_> {
(merged.keep, merged.pushes, merged.snapshot) = (keep, pushes, frozen);
(merged.dead, merged.corpses, merged.gone) = (dead, corpses, gone);
merged.guest_events = guest_events;
#[cfg(test)]
{
diag::LAST.lock().unwrap().merge_ns = merge_began.elapsed().as_nanos() as u64;
}
if let Some((serial, seeds)) = serial {
let diff = difference(&serial, &merged)
.or_else(|| (!seeds.complete()).then(|| "new RNG allocation counts differ".into()));
@@ -444,11 +378,6 @@ impl Islands<'_> {
difficulty,
cached_difficulty: None,
};
#[cfg(test)]
let (began, cpu0) = {
horizon_core::random::take_waited();
(std::time::Instant::now(), diag::thread_cpu_ns())
};
if let Some(seeds) = seeds {
horizon_client::random_source::replay_seeds(Arc::clone(seeds), || {
g.run(&mut turns, shared, Some(&foreign));
@@ -456,14 +385,6 @@ impl Islands<'_> {
} else {
g.run(&mut turns, shared, Some(&foreign));
}
#[cfg(test)]
diag::LAST.lock().unwrap().lanes.push(diag::Lane {
server: g.global[0] == 0,
mobs: g.mobs.len(),
busy_ns: began.elapsed().as_nanos() as u64,
cpu_ns: diag::thread_cpu_ns() - cpu0,
waited_ns: horizon_core::random::take_waited().as_nanos() as u64,
});
};
let tickings: Vec<_> = groups.iter().map(|_| (self.ticking)()).collect();
let mut parts = groups.iter_mut().zip(tickings);
@@ -523,12 +444,8 @@ impl Islands<'_> {
.expect("a mob island's thread starts");
handles.push(handle);
}
#[cfg(test)]
let own_began = std::time::Instant::now();
run(first, first_ticking);
drop(first_done);
#[cfg(test)]
let own_done = std::time::Instant::now();
let mut panic = None;
for handle in handles {
match handle.join() {
@@ -537,12 +454,6 @@ impl Islands<'_> {
Err(_) => {}
}
}
#[cfg(test)]
{
let mut d = diag::LAST.lock().unwrap();
d.join_wait_ns = own_done.elapsed().as_nanos() as u64;
d.threads_ns = own_began.elapsed().as_nanos() as u64;
}
if let Some(payload) = panic {
std::panic::resume_unwind(payload);
}
@@ -1076,12 +987,6 @@ fn islands(reaches: &[Reach], lanes: usize) -> Option<Plan> {
islands[k].1.push(i);
islands[k].0 += usize::from(r.ticking);
}
#[cfg(test)]
{
let mut w: Vec<usize> = islands.iter().map(|i| i.0).collect();
w.sort_unstable_by(|a, b| b.cmp(a));
*diag::ISLANDS.lock().unwrap() = (ticking, w);
}
let largest = islands.iter().map(|i| i.0).max().unwrap_or(0);
if largest * 4 > ticking * 3 {
return None;
@@ -2167,31 +2072,6 @@ mod tests {
}
}
/// With the villagers on their own random (`parallel::villagers_own_random`), villagers in one
/// herd tick in groups beside the other herds and leave the mobs and the level random as the
/// serial loop does, since the serial loop draws the same way while the switch is on.
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn villagers_on_their_own_random_tick_in_groups_as_serial() {
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
horizon_client::parallel::OWN_RANDOM_TEST
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
let _reset = Reset;
horizon_client::parallel::OWN_RANDOM_TEST.store(true, std::sync::atomic::Ordering::Relaxed);
let mut game = separated_herds_game();
for k in 0..12 {
let x = 4.0 + f64::from(k % 6) * 2.0;
let z = 14.0 + f64::from(k / 6) * 2.0;
assert!(game.run_command(&format!("summon villager {x:.1} 66 {z:.1}")));
}
let groups = grouped_as_serial(&mut game, 30, "villagers", |_| {});
assert_eq!(groups, groups_for(3));
}
/// The raiders of one raid order their hurts across it (`raids.rs`): one in each of two herds
/// joins them, and the herd between ticks in a group of its own.
#[test]
@@ -2314,223 +2194,4 @@ mod tests {
assert_ne!(of[0], of[200]);
assert_eq!(of[200], of[300], "both small herds fill the lighter group");
}
/// A level shaped like a busy survival server: many small chicken farms, bee and villager
/// clusters, sulfur cubes, and monsters scattered between them, all far apart. Prints, for
/// each lane setting (alternating tick by tick), the mob phase's wall time, CPU time and the
/// server thread's wait, and where the lanes spent their time.
///
/// `PM_LANES=1,4 PM_TICKS=300 PM_WARM=60 PM_FARMS=40 PM_CHICKENS=18`, run with
/// `--ignored --exact --nocapture --test-threads=1`.
#[test]
#[ignore = "a measurement; needs prepared resources in HORIZON_TEST_RESOURCES"]
fn pm_shaped_bench() {
let var = |k: &str, d: usize| {
std::env::var(k)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(d)
};
let (ticks, warm) = (var("PM_TICKS", 300), var("PM_WARM", 60));
let lane_settings: Vec<usize> = std::env::var("PM_LANES")
.unwrap_or_else(|_| "1,4".into())
.split(',')
.filter_map(|v| v.parse().ok())
.collect();
horizon_client::parallel::OWN_RANDOM_TEST
.store(var("PM_OWN", 0) == 1, std::sync::atomic::Ordering::Relaxed);
let farms = var("PM_FARMS", 40);
let per_farm = var("PM_CHICKENS", 18);
let mut game = super::super::super::native_machines::tests::server_game();
game.build_registries();
game.set_allow_commands(true);
let span = 800;
let mid = f64::from(span) / 2.0;
game.player = Some(
horizon_client::motion::PlayerMotion::new(game.motion, Vec3::new(mid, 65.0, mid))
.unwrap(),
);
for x in -3..=(span / 16 + 3) {
for z in -3..=(span / 16 + 3) {
game.world
.chunks
.insert((x, z), Arc::new(horizon_client::chunk::Chunk::empty(x, z)));
}
}
game.update_chunk_tickets();
let _ = game.run_command("difficulty normal");
let stone = game
.pack
.assets
.registry
.default_state("minecraft:stone")
.unwrap();
// Cells of 80 blocks, shuffled by a fixed generator; each cluster takes one.
let mut state = 0x2545_f491_4f6c_dd1d_u64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let mut cells: Vec<(i32, i32)> = (0..9)
.flat_map(|x| (0..9).map(move |z| (x, z)))
.map(|(x, z)| (40 + x * 80, 40 + z * 80))
.collect();
for i in (1..cells.len()).rev() {
cells.swap(i, (next() % (i as u64 + 1)) as usize);
}
let mut cluster = |game: &mut Game<'static>, kind: &str, count: usize, extra: &str| {
let (cx, cz) = cells.pop().expect("a free cell");
for x in cx..cx + 12 {
for z in cz..cz + 12 {
game.world.set(x, 64, z, stone).unwrap();
}
}
for k in 0..count {
let x = f64::from(cx) + 1.0 + (k % 10) as f64 * 1.0;
let z = f64::from(cz) + 1.0 + (k / 10) as f64 * 1.0;
let c = format!("summon {kind} {x:.2} 66 {z:.2} {extra}");
assert!(game.run_command(c.trim()), "{c}");
}
};
for _ in 0..farms {
cluster(&mut game, "chicken", per_farm, "");
}
for _ in 0..var("PM_BEE_GROUPS", 8) {
cluster(&mut game, "bee", var("PM_BEES", 95), "");
}
for _ in 0..var("PM_VILLAGES", 3) {
cluster(&mut game, "villager", var("PM_VILLAGERS", 90), "");
}
for _ in 0..var("PM_CUBE_GROUPS", 6) {
cluster(&mut game, "sulfur_cube", var("PM_CUBES", 24), "");
}
// Monsters scattered anywhere, one by one.
for k in 0..var("PM_MONSTERS", 120) {
let kind = ["husk", "husk", "skeleton", "spider", "creeper"][k % 5];
let x = 20.0 + (next() % (span as u64 - 40)) as f64;
let z = 20.0 + (next() % (span as u64 - 40)) as f64;
let (bx, bz) = (x as i32, z as i32);
for dx in 0..3 {
for dz in 0..3 {
game.world.set(bx + dx, 64, bz + dz, stone).unwrap();
}
}
if !game.run_command(&format!("summon {kind} {x:.2} 66 {z:.2}")) {
eprintln!("summon {kind} {x:.2} 66 {z:.2} failed");
}
}
let _ = game.run_command("difficulty normal");
let mut by_kind = std::collections::BTreeMap::<String, usize>::new();
for m in &game.mobs {
*by_kind.entry(format!("{:?}", m.kind.species)).or_default() += 1;
}
eprintln!("mobs: {} {by_kind:?}", game.mobs.len());
struct Reset;
impl Drop for Reset {
fn drop(&mut self) {
LANES.with(|l| l.set(None));
horizon_client::parallel::OWN_RANDOM_TEST
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
let _reset = Reset;
#[derive(Default)]
struct Sum {
n: usize,
grouped: usize,
wall: Vec<f64>,
cpu: f64,
join: f64,
plan: f64,
setup: f64,
merge: f64,
lanes: Vec<(f64, f64, f64, f64, f64)>,
groups: Vec<usize>,
}
let mut sums: Vec<Sum> = lane_settings.iter().map(|_| Sum::default()).collect();
for t in 0..warm + ticks {
let which = t % lane_settings.len();
LANES.with(|l| l.set(Some(lane_settings[which])));
RAN.with(|r| r.set(0));
diag::LAST.lock().unwrap().lanes.clear();
let cpu0 = diag::thread_cpu_ns();
let began = std::time::Instant::now();
game.tick_creature_entities().unwrap();
let wall = began.elapsed().as_secs_f64() * 1e3;
let mut cpu = (diag::thread_cpu_ns() - cpu0) as f64 / 1e6;
if t < warm {
continue;
}
let s = &mut sums[which];
s.n += 1;
s.wall.push(wall);
s.plan += diag::PLAN_NS.load(std::sync::atomic::Ordering::Relaxed) as f64 / 1e6;
let ran = RAN.with(Cell::get);
if ran > 0 {
s.grouped += 1;
let d = diag::LAST.lock().unwrap().clone();
s.join += d.join_wait_ns as f64 / 1e6;
s.setup += d.setup_ns as f64 / 1e6;
s.merge += d.merge_ns as f64 / 1e6;
for l in &d.lanes {
if !l.server {
cpu += l.cpu_ns as f64 / 1e6;
}
}
let mut ls: Vec<_> = d.lanes.clone();
ls.sort_by_key(|l| (!l.server, std::cmp::Reverse(l.mobs)));
if s.lanes.len() < ls.len() {
s.lanes.resize(ls.len(), (0.0, 0.0, 0.0, 0.0, 0.0));
}
for (acc, l) in s.lanes.iter_mut().zip(&ls) {
acc.0 += l.busy_ns as f64 / 1e6;
acc.1 += l.cpu_ns as f64 / 1e6;
acc.2 += l.waited_ns as f64 / 1e6;
acc.3 += l.mobs as f64;
acc.4 += 1.0;
}
s.groups = d.groups.clone();
}
s.cpu += cpu;
}
let (ticking, weights) = diag::ISLANDS.lock().unwrap().clone();
eprintln!(
"ticking {ticking}, {} islands, heaviest {:?}",
weights.len(),
&weights[..weights.len().min(12)]
);
for (s, lanes) in sums.iter_mut().zip(&lane_settings) {
s.wall.sort_by(|a, b| a.total_cmp(b));
let q = |p: f64| s.wall[((s.wall.len() - 1) as f64 * p) as usize];
let n = s.n as f64;
let g = s.grouped.max(1) as f64;
eprintln!(
"lanes {lanes}: wall mean {:.2} p50 {:.2} p95 {:.2} | cpu {:.2} | grouped {}/{} | plan {:.2} setup {:.2} server-join-wait {:.2} merge {:.2} (per grouped tick)",
s.wall.iter().sum::<f64>() / n,
q(0.5),
q(0.95),
s.cpu / n,
s.grouped,
s.n,
s.plan / n,
s.setup / g,
s.join / g,
s.merge / g
);
eprintln!(" group sizes (last grouped tick) {:?}", s.groups);
for (i, l) in s.lanes.iter().enumerate() {
let k = l.4.max(1.0);
eprintln!(
" lane {i}{}: mobs {:.0} busy {:.2} ms, cpu {:.2} ms, waited on draws {:.2} ms",
if i == 0 { " (server)" } else { "" },
l.3 / k,
l.0 / k,
l.1 / k,
l.2 / k
);
}
}
}
}
+1 -1
View File
@@ -1956,7 +1956,7 @@ fn server_ai_step(
}
if m.species() == Species::Villager {
// Villager.customServerAiStep: the brain, then the raid sound roll.
super::villager::tick_brain(m, w, level_random, events);
super::villager_brain::tick(m, w, level_random, events);
if let Some(player) = super::villager::ext_mut(m).last_traded_player.take() {
events.push(MobEvent::VillagerTrade { mob: m.id, player });
}
@@ -1079,22 +1079,6 @@ pub fn read_nbt(m: &mut Mob, tag: &horizon_world::nbt::Tag) {
}
}
/// The villager's brain tick. While the parallel mob tick is on it draws from the villager's own random, so
/// that no other mob's turn is waited for (`parallel::villagers_own_random`); the level's random otherwise.
pub fn tick_brain(
m: &mut Mob,
w: &MobWorld,
level_random: &mut horizon_core::random::LegacyRandom,
events: &mut Vec<MobEvent>,
) {
if crate::parallel::villagers_own_random() {
let mut own = horizon_core::random::LegacyRandom::new(m.random.next_long());
super::villager_brain::tick(m, w, &mut own, events);
} else {
super::villager_brain::tick(m, w, level_random, events);
}
}
#[cfg(test)]
mod tests {
use super::*;
-18
View File
@@ -16,24 +16,6 @@ pub fn parallel_tick() -> u8 {
})
}
/// Whether a villager's brain draws from the villager's own random instead of the level's.
///
/// The level random is one stream, so a draw has to wait for every mob before it (`OrderedDraws`),
/// and a villager's brain draws on nearly every turn (behaviour durations, one-in-N rolls).
/// With the parallel tick on, those draws come from a stream seeded from the villager's own, so
/// no villager waits on another mob and the lanes run apart. That is the one place the parallel
/// tick departs from vanilla's draw order: the villagers' rolls have the same odds but not the
/// same values a vanilla level would give them. The serial loop does the same while the parallel
/// tick is on, so which path a tick takes never changes the result.
pub fn villagers_own_random() -> bool {
OWN_RANDOM_TEST.load(Ordering::Relaxed) || parallel_tick() > 0
}
/// A test's switch for [`villagers_own_random`], process-wide because the lanes are threads.
#[doc(hidden)]
pub static OWN_RANDOM_TEST: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
/// How many threads a phase may share its work among: `horizon-threads`' count (at most 8) while
/// the parallel tick is on, else 1.
pub fn lanes() -> usize {
+1 -1
View File
@@ -5,7 +5,7 @@ mod owner;
mod positional;
mod worldgen;
pub use hash::{seed_from_hash, seed_from_hash_utf16};
pub use ordered::{LaneDone, OrderedDraws, take_waited};
pub use ordered::{LaneDone, OrderedDraws};
pub use owner::{EntityPositional, EntityRandom, LevelPositional, LevelRandom};
pub use positional::{
LegacyPositionalRandom, PositionalRandom, XoroshiroPositionalRandom, java_string_hash,
+24 -121
View File
@@ -10,31 +10,10 @@
//! lane has its own thread. An item that never draws or reseeds never waits.
use super::LegacyRandom;
use std::sync::{
Arc, Condvar, Mutex, PoisonError,
Arc, Mutex, PoisonError,
atomic::{AtomicUsize, Ordering},
};
thread_local! {
/// Nanoseconds this thread has spent waiting for other lanes' items to finish.
static WAITED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// The time this thread has spent waiting on other lanes since it last asked (for the first draw
/// of an item, a lookup of a mob in another group, a new seed), and starts the count again.
pub fn take_waited() -> std::time::Duration {
std::time::Duration::from_nanos(WAITED.with(|w| w.replace(0)))
}
/// Adds the time since it was made to this thread's wait count when dropped; made only once a
/// wait is certain, so a wait that isn't needed costs nothing.
struct Waiting(std::time::Instant);
impl Drop for Waiting {
fn drop(&mut self) {
let ns = self.0.elapsed().as_nanos() as u64;
WAITED.with(|w| w.set(w.get().saturating_add(ns)));
}
}
/// The shared stream and where each lane is.
#[derive(Debug)]
pub struct OrderedDraws {
@@ -42,17 +21,8 @@ pub struct OrderedDraws {
state: Mutex<(u64, Option<f64>)>,
/// The item each lane is on; `usize::MAX` once it has finished.
lanes: Box<[AtomicUsize]>,
/// Lanes asleep in `wait_until`.
sleepers: AtomicUsize,
parked: Mutex<()>,
wake_up: Condvar,
}
/// Spins a wait makes before it yields its core.
const SPINS: u32 = 200;
/// How long a wait yields its core before it sleeps.
const YIELD_FOR: std::time::Duration = std::time::Duration::from_micros(20);
/// A lane's hold on the shared stream, inside its [`LegacyRandom`].
#[derive(Debug)]
pub(super) struct Gate {
@@ -68,9 +38,6 @@ impl OrderedDraws {
Arc::new(Self {
state: Mutex::new((random.state, random.gaussian)),
lanes: (0..lanes).map(|_| AtomicUsize::new(0)).collect(),
sleepers: AtomicUsize::new(0),
parked: Mutex::new(()),
wake_up: Condvar::new(),
})
}
/// The random lane `lane` draws from: its draws wait for their turn.
@@ -93,57 +60,34 @@ impl OrderedDraws {
}
/// Wait until lane `lane` has finished item `item` (it is on a later one, or done).
pub fn wait_past(&self, lane: usize, item: usize) {
self.wait_until(|| self.lanes[lane].load(Ordering::Acquire) > item);
let mut spins = 0u32;
while self.lanes[lane].load(Ordering::Acquire) <= item {
spins += 1;
if spins < 64 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
}
fn wait_for_turn(&self, lane: usize, item: usize) {
self.wait_until(|| {
self.lanes
let mut spins = 0u32;
loop {
let ready = self
.lanes
.iter()
.enumerate()
.all(|(k, at)| k == lane || at.load(Ordering::Acquire) > item)
});
}
/// Returns once `ready` holds. A wait is mostly short, so it spins first. The lane waited on
/// may be a thread the machine has not given a core (the helpers compete with the server's
/// other threads), and a waiter that keeps its core then holds that lane up: after a moment
/// the waiter sleeps until a lane moves on ([`Self::wake`]).
fn wait_until(&self, ready: impl Fn() -> bool) {
if ready() {
return;
}
let _waiting = Waiting(std::time::Instant::now());
for _ in 0..SPINS {
std::hint::spin_loop();
if ready() {
.all(|(k, at)| k == lane || at.load(Ordering::Acquire) > item);
if ready {
return;
}
}
let until = std::time::Instant::now() + YIELD_FOR;
while std::time::Instant::now() < until {
std::thread::yield_now();
if ready() {
return;
spins += 1;
if spins < 64 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
}
self.sleepers.fetch_add(1, Ordering::SeqCst);
let mut guard = self.parked.lock().unwrap_or_else(PoisonError::into_inner);
while !ready() {
// The timeout only guards against a wake that was missed; `wake` takes this lock.
guard = self
.wake_up
.wait_timeout(guard, std::time::Duration::from_millis(1))
.unwrap_or_else(PoisonError::into_inner)
.0;
}
drop(guard);
self.sleepers.fetch_sub(1, Ordering::SeqCst);
}
/// A lane has moved on (or finished): wakes the lanes that sleep. Cheap while none does.
fn wake(&self) {
if self.sleepers.load(Ordering::SeqCst) > 0 {
drop(self.parked.lock().unwrap_or_else(PoisonError::into_inner));
self.wake_up.notify_all();
}
}
}
@@ -169,8 +113,7 @@ impl LegacyRandom {
pub fn begin_item(&mut self, item: usize) {
if let Some(gate) = self.gate.as_deref_mut() {
gate.item = item;
gate.draws.lanes[gate.lane].store(item, Ordering::SeqCst);
gate.draws.wake();
gate.draws.lanes[gate.lane].store(item, Ordering::Release);
}
}
/// The item's draws are done: the shared stream goes on from where it left it.
@@ -190,8 +133,7 @@ impl LegacyRandom {
pub fn finish_lane(&mut self) {
self.end_item();
if let Some(gate) = self.gate.as_deref_mut() {
gate.draws.lanes[gate.lane].store(usize::MAX, Ordering::SeqCst);
gate.draws.wake();
gate.draws.lanes[gate.lane].store(usize::MAX, Ordering::Release);
}
}
/// A gated random's [`LaneDone`], for its lane's thread to hold while it runs; None for an
@@ -215,8 +157,7 @@ pub struct LaneDone {
impl Drop for LaneDone {
fn drop(&mut self) {
self.draws.lanes[self.lane].store(usize::MAX, Ordering::SeqCst);
self.draws.wake();
self.draws.lanes[self.lane].store(usize::MAX, Ordering::Release);
}
}
@@ -241,44 +182,6 @@ mod tests {
}
}
/// A lane that waits long enough to sleep (the lane before it is slow) still draws after it
/// and gets the serial stream, and wakes when that lane moves on or finishes.
#[test]
fn a_long_wait_sleeps_and_wakes_in_order() {
let mut serial = LegacyRandom::new(5);
let expected = [serial.next_int() as u64, serial.next_int() as u64];
let got = bounded(|| {
let shared = OrderedDraws::new(&LegacyRandom::new(5), 2);
std::thread::scope(|s| {
let slow = {
let shared = Arc::clone(&shared);
s.spawn(move || {
let mut random = shared.lane(0);
random.begin_item(0);
std::thread::sleep(std::time::Duration::from_millis(50));
let first = random.next_int() as u64;
random.end_item();
random.finish_lane();
first
})
};
let fast = {
let shared = Arc::clone(&shared);
s.spawn(move || {
let mut random = shared.lane(1);
random.begin_item(1);
let second = random.next_int() as u64;
random.end_item();
random.finish_lane();
second
})
};
[slow.join().unwrap(), fast.join().unwrap()]
})
});
assert_eq!(got, expected);
}
#[test]
fn reseeding_lanes_preserve_the_serial_stream() {
let mut initial = LegacyRandom::new(42);
-19
View File
@@ -115,25 +115,6 @@ pending vibration as mob ids, read and angered at any distance: every id in its
The suspects it keeps by UUID resolve only to the host's player during the loop, serially as in
groups (`LoopLookup`).
## Villagers draw their own random
The ordered level random (above) makes a mob's first draw wait for every mob before it in the
list, on every group. A villager's brain draws on nearly every turn (behaviour durations in
`Timed::begin`, one-in-N rolls such as `nextInt(20) != 0`), and the result of most rolls decides
what the villager does in the same turn, so those draws cannot be put off. With
`HORIZON_PARALLEL_TICK` on, the brain therefore draws from a `LegacyRandom` seeded from one
`next_long` of the villager's own random (`parallel::villagers_own_random`, `mobs/ai.rs`), not
from the level random. No villager waits for another mob's draws, and a measured world whose
only level-random draws were villagers' had none left to wait for.
This is a deliberate departure from vanilla's draw order, accepted for the parallel tick: the
odds of every roll are vanilla's, the values are not the ones a vanilla level would give, and the
level random is no longer advanced by villagers. The serial loop draws the same way while the
switch is on, so the grouped result still equals the serial result and `verify` compares like
with like. With the parallel tick off nothing changes, and the villager parity harnesses (which
pin vanilla's exact draws) run with it off. A test turns the switch on with
`parallel::OWN_RANDOM_TEST`.
## Native mods and the profiler
What a mod can run in a mob's turn: `native_living_change_target`, `native_living_heal`,
+1
View File
@@ -72,6 +72,7 @@ A harness can declare, at the top level of its script:
- `PARITY_SHARDS = N`: it runs as `NAME#1` to `NAME#N`, in parallel, each told its share in `PARITY_SHARD=i/N` (`test_datafix_parity.py` upgrades every sixth world). `NAME` on the command line or in `--known-failures` means all of them.
- `PARITY_RUST_SCOPE = [...]`: the Rust sources its probes can reach, as path prefixes. Cargo's dep-info names every file of every crate a probe links, so without it a probe of `horizon-world` depends on all 350 of its files. The runner keeps only the scope's files if the scope is closed: nothing in it names a module, crate-root re-export or macro outside it (`crate::`, `super::`, other workspace crates; comments and strings ignored), declares a module it doesn't cover, or includes source, and nothing outside it implements anything for a type it defines. Crate roots then count without their declarations of other modules, so adding a module elsewhere doesn't rerun the harness. If the scope isn't closed the runner says why and keeps every file. `datafix` depends on 41 files this way, not 354.
- `PARITY_MANUAL = "<how to run it>"`: the script is a proof someone runs by hand with arguments (a Java side, then a native side against its reference), not a harness. `parity.py` leaves it out of every run, `--changed` and parity-main included, and names that text when it is asked for it by name.
### Which harnesses a change affects
+9
View File
@@ -413,6 +413,15 @@ class ShardTests(unittest.TestCase):
self.assertEqual(parity.SHARDS["x#2"], "2/3")
self.assertEqual(parity.base_name("x#2"), "x")
def test_a_manual_proof_script_is_left_out(self):
with tempfile.TemporaryDirectory() as temp, mock.patch.object(parity, "ROOT", Path(temp)), \
mock.patch.dict(parity.MANUAL, clear=True):
(Path(temp) / "tools").mkdir()
(Path(temp) / "tools/test_x_parity.py").write_text('import sys\nPARITY_MANUAL = "--side java, then --side native"\n')
(Path(temp) / "tools/test_y_parity.py").write_text("")
self.assertEqual(sorted(parity.harnesses()), ["y"])
self.assertEqual(parity.MANUAL, {"x": "--side java, then --side native"})
class OracleArgumentTests(unittest.TestCase):
def test_an_agent_jar_is_an_input(self):
+11
View File
@@ -72,5 +72,16 @@ class SaveRoundtripPathsTests(unittest.TestCase):
self.assertEqual(roundtrip.summarize(report), 2)
class SaveRoundtripTimersTests(unittest.TestCase):
def test_elapsed_despawn_timers_do_not_hide_other_saved_state_changes(self):
report = [
('entity.DespawnDelay @minecraft:trader_llama#57', 47384, 47375),
('entity.Passengers[].DespawnDelay @minecraft:trader_llama#58', 100, 91),
('entity.PersistenceRequired @minecraft:trader_llama#57', 0, 1),
]
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(roundtrip.summarize(report), 1)
if __name__ == '__main__':
unittest.main()
+13 -4
View File
@@ -90,27 +90,33 @@ FAILURE_TAILS, FAILURE_TAIL_LINES = 10, 30 # the summary prints the end of the
def harness_settings(path):
"""PARITY_SHARDS and PARITY_RUST_SCOPE, read from a harness's source without running it."""
"""PARITY_SHARDS, PARITY_RUST_SCOPE and PARITY_MANUAL, read from a harness's source without running it."""
import ast
text = Path(path).read_text(encoding="utf-8")
settings = {}
if "PARITY_" in text:
for node in ast.parse(text).body:
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name) \
and node.targets[0].id in ("PARITY_SHARDS", "PARITY_RUST_SCOPE"):
and node.targets[0].id in ("PARITY_SHARDS", "PARITY_RUST_SCOPE", "PARITY_MANUAL"):
settings[node.targets[0].id] = ast.literal_eval(node.value)
return settings
SHARDS = {} # "datafix#2" -> "2/8": the share of a sharded harness each entry runs (PARITY_SHARD)
MANUAL = {} # "guest_close_event" -> how to run it: a proof script that needs arguments, which no run starts
def harnesses():
"""Harness names and scripts. A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1 to NAME#N."""
"""Harness names and scripts. A harness declaring PARITY_SHARDS = N runs as N entries, NAME#1 to NAME#N.
A script declaring PARITY_MANUAL = "<how to run it>" is a proof run by hand with arguments: left out (MANUAL)."""
found = {}
for path in sorted((ROOT / "tools").glob("test_*_parity.py")):
name = path.name[len("test_"):-len("_parity.py")]
count = harness_settings(path).get("PARITY_SHARDS", 1)
settings = harness_settings(path)
if settings.get("PARITY_MANUAL"):
MANUAL[name] = settings["PARITY_MANUAL"]
continue
count = settings.get("PARITY_SHARDS", 1)
if count > 1:
for index in range(1, count + 1):
found[f"{name}#{index}"] = path
@@ -1652,6 +1658,9 @@ def main():
# A sharded harness's name stands for all its shards.
args.names = [entry for name in args.names for entry in ([name] if name in found else
[shard for shard in found if base_name(shard) == name] or [name])]
by_hand = [name for name in args.names if name in MANUAL]
if by_hand:
parser.error("; ".join(f"{name} is run by hand, not by parity.py: {MANUAL[name]}" for name in by_hand))
unknown = [name for name in args.names if name not in found]
if unknown:
parser.error("unknown harnesses: " + ", ".join(unknown))
+4 -2
View File
@@ -16,7 +16,8 @@ except ImportError:
import lab
import test_rust_parity as base
TEST = 'level::tests::fresh_chunk_'
# The sculk regression lives in level::tests::trigger_tests after the test split.
TEST = 'fresh_chunk_'
def main():
@@ -41,7 +42,8 @@ def main():
lab.ROOT / 'crates/horizon-world/src/level/mod.rs',
lab.ROOT / 'crates/horizon-world/src/level/pistons.rs',
lab.ROOT / 'crates/horizon-world/src/level/sculk.rs',
lab.ROOT / 'crates/horizon-world/src/level/tests.rs']
lab.ROOT / 'crates/horizon-world/src/level/tests.rs',
lab.ROOT / 'crates/horizon-world/src/level/tests/trigger_tests.rs']
hashes = base.source_hashes(sources)
def run(command, label, **kwargs):
@@ -17,6 +17,7 @@ import struct
import subprocess
import time
import uuid
PARITY_MANUAL = "a proof run by hand, once per side: --side java, then --side native, each with --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import anvil_dump as nbt, net_capture, test_locator_bar_parity as net
except ImportError:
@@ -1,6 +1,7 @@
"""Actual named movement predicates over a frozen, UUID-bound unmounted pig."""
import argparse,hashlib,json,sys
from pathlib import Path
PARITY_MANUAL = "a proof run by hand: --work-dir DIR --reference-only for the Java side, then --work-dir DIR2 --reference-dir DIR for the native one" # not a harness: tools/parity.py leaves it out
try:
from . import test_command_exec_parity as driver
except ImportError:
+1
View File
@@ -11,6 +11,7 @@ import json
from pathlib import Path
import subprocess
import sys
PARITY_MANUAL = "a proof run by hand: --side java --work-dir DIR, then --side native --work-dir DIR2 --binary BIN --reference DIR/vanilla.jsonl" # not a harness: tools/parity.py leaves it out
ROOT = Path(__file__).resolve().parents[1]
@@ -14,6 +14,7 @@ import hashlib
import struct
import uuid
import re
PARITY_MANUAL = "a proof run by hand, once per side: --side java, then --side native, each with --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import anvil_dump as nbt, net_capture, test_locator_bar_parity as net
+9 -7
View File
@@ -24,18 +24,20 @@ except ImportError:
NAME = 'mob-display-name-parity'
SCRIPT = [
('setup', ['chat on']),
('summon', ['cmd Alice summon minecraft:wolf 2.5 -60 4.5 {NoAI:1b}']),
('named', ['cmd Alice summon minecraft:pig 4.5 -60 4.5 {NoAI:1b,CustomName:"Bob"}']),
# Only the script's own mobs may match its selectors. Both servers load a
# real generated world and may otherwise spawn different numbers of pigs.
('setup', ['chat on', 'cmd Alice gamerule spawn_mobs false']),
('summon', ['cmd Alice summon minecraft:wolf 2.5 -60 4.5 {NoAI:1b,Tags:["display_name_parity"]}']),
('named', ['cmd Alice summon minecraft:pig 4.5 -60 4.5 {NoAI:1b,CustomName:"Bob",Tags:["display_name_parity"]}']),
# /team names the team with PlayerTeam.getFormattedDisplayName and its member with
# ScoreHolder.getFeedbackDisplayName (the display name, its scoreboard name shown on hover).
('team', ['cmd Alice team add reds', 'cmd Alice team modify reds color red',
'cmd Alice team join reds @e[type=minecraft:pig]']),
('data', ['cmd Alice data get entity @e[type=minecraft:pig,limit=1] Health']),
('kill', ['cmd Alice kill @e[type=minecraft:wolf]']),
'cmd Alice team join reds @e[type=minecraft:pig,tag=display_name_parity]']),
('data', ['cmd Alice data get entity @e[type=minecraft:pig,tag=display_name_parity,limit=1] Health']),
('kill', ['cmd Alice kill @e[type=minecraft:wolf,tag=display_name_parity]']),
# /scoreboard names score holders with getFeedbackDisplayName too: an entity's UUID, a player's name on hover.
('scores', ['cmd Alice scoreboard objectives add points dummy',
'cmd Alice scoreboard players set @e[type=minecraft:pig] points 3',
'cmd Alice scoreboard players set @e[type=minecraft:pig,tag=display_name_parity] points 3',
'cmd Alice scoreboard players set Alice points 1']),
]
INTS = re.compile(r'"uuid":\[(-?\d+),(-?\d+),(-?\d+),(-?\d+)\]')
@@ -31,6 +31,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
import lab # noqa: E402
PARITY_MANUAL = "a proof run by hand: --kind java --output DIR, then --kind native --output DIR2 --binary BIN" # not a harness: tools/parity.py leaves it out
META = '{"pack":{"description":"predicate reference graph","min_format":[121,0],"max_format":[121,0]}}\n'
# 26.3 dispatches a predicate on `type`; a named reference is a holder: an id where a predicate goes.
+1 -1
View File
@@ -86,7 +86,7 @@ IGNORED = {
r'server_data\.state_updating_resumes_at)$':
'block entity timers advance',
r'^entity\.(Passengers\[\]\.)*(Age|Motion\[\]|Rotation\[\]|Pos\[\]|OnGround|HurtTime|Health|TimeInOverworld|fall_distance|'
r'wasOnGround|Fire|Air|DeathTime|Fuse|life|inGround|ticks_in_ground|Time|EggLayTime)$': 'entity simulation advances',
r'wasOnGround|Fire|Air|DeathTime|Fuse|life|inGround|ticks_in_ground|Time|EggLayTime|DespawnDelay)$': 'entity simulation advances',
r'^level\.dat\.Data\.(Time|LastPlayed)$': 'clock and wall time of the save',
r'^level\.dat\.Data\.(ServerBrands.*|singleplayer_uuid)$': 'a client world names its brand and its player',
r'^entity\.listener\.selector\.(event|tick)$': 'the warden listener holds the last vibration it heard; timing',
+1
View File
@@ -4,6 +4,7 @@ Initial default delivery is observational; flags never stand in for a typed Pose
"""
import argparse,hashlib,json,os,re,time
from pathlib import Path
PARITY_MANUAL = "a proof run by hand: --work-dir DIR --reference-only for the Java side, then --work-dir DIR2 --reference-dir DIR --binary BIN" # not a harness: tools/parity.py leaves it out
try:
from . import net_capture,test_locator_bar_parity as net
from .test_builtin_component_delivery_parity import Commands,generate,offline,uuid_words