diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs index 35814dd6c..affc120e7 100644 --- a/crates/horizon-client/src/game.rs +++ b/crates/horizon-client/src/game.rs @@ -324,7 +324,10 @@ impl Options { dedicated: None, benchmark: None, dev_mods: Vec::new(), - server_thread: std::env::var("HORIZON_SERVER_THREAD").map_or(true, |v| v != "0"), + // The command harness (HORIZON_COMMAND_SCRIPT) drives the world + // itself, without the event loop: it stays in the frame. + server_thread: std::env::var("HORIZON_SERVER_THREAD").map_or(true, |v| v != "0") + && std::env::var_os("HORIZON_COMMAND_SCRIPT").is_none(), integrated: false, }; let mut session_file = std::env::var("HORIZON_SESSION_FILE").ok(); diff --git a/crates/horizon-client/src/game/control.rs b/crates/horizon-client/src/game/control.rs index 6c69904ac..28e6c56c0 100644 --- a/crates/horizon-client/src/game/control.rs +++ b/crates/horizon-client/src/game/control.rs @@ -879,7 +879,9 @@ impl Game<'_> { "tridents": self.tridents.len(), "rockets": self.rockets.len(), "fight": self.dragon_status(), - "carts": self.carts.iter().map(|c| serde_json::json!({"kind": c.kind.id(), "position": [c.position.x, c.position.y, c.position.z], "velocity": [c.velocity.x, c.velocity.y, c.velocity.z]})).collect::>(), + // A client of a server (single player's server thread) knows the + // carts it was sent. + "carts": self.remote_carts().unwrap_or_else(|| self.carts.iter().map(|c| serde_json::json!({"kind": c.kind.id(), "position": [c.position.x, c.position.y, c.position.z], "velocity": [c.velocity.x, c.velocity.y, c.velocity.z]})).collect::>()), "riding": self.riding, "using": self.using, "render_distance": self.streaming.radius, diff --git a/crates/horizon-client/src/game/remote.rs b/crates/horizon-client/src/game/remote.rs index f7346199a..b770e71a9 100644 --- a/crates/horizon-client/src/game/remote.rs +++ b/crates/horizon-client/src/game/remote.rs @@ -529,6 +529,28 @@ impl Game<'_> { } } + /// The minecarts the server sent, for the control socket's `status` + /// (their velocity is not tracked here). + pub(super) fn remote_carts(&self) -> Option> { + let r = self.remote.as_ref()?; + let mut carts: Vec<&Tracked> = r + .entities + .values() + .filter(|t| t.kind.ends_with("minecart")) + .collect(); + carts.sort_by(|a, b| { + a.position + .partial_cmp(&b.position) + .unwrap_or(std::cmp::Ordering::Equal) + }); + Some( + carts + .into_iter() + .map(|t| serde_json::json!({"kind": t.kind, "position": t.position, "velocity": [0.0, 0.0, 0.0]})) + .collect(), + ) + } + /// The names in the server's player list, sorted. pub(super) fn remote_online_names(&self) -> Option> { let r = self.remote.as_ref()?; diff --git a/tools/test_save_roundtrip_parity.py b/tools/test_save_roundtrip_parity.py index e2531a657..72bea8845 100644 --- a/tools/test_save_roundtrip_parity.py +++ b/tools/test_save_roundtrip_parity.py @@ -68,6 +68,14 @@ IGNORED = { # differently (no worldgen heightmaps, entities in entities/) is accepted. r'^chunk\.promoted\.(Status|Heightmaps\.(OCEAN_FLOOR_WG|WORLD_SURFACE_WG)|entities)$': 'a proto chunk vanilla left at its view distance, finished by Horizon', + # Single player's server thread sends the chunks its player tracks (view distance 6 + # and its outer ring), as vanilla's integrated server does, so it finishes some that + # vanilla's server (view distance 4) left before their terrain: those had no blocks, + # light or heightmaps to compare yet. + r'^chunk\.blank\.': + 'a chunk vanilla left before its terrain (no blocks yet), finished by Horizon', + r'^chunk\.unlit\.': + 'a chunk vanilla left before its light, finished (and lit) by Horizon', # Horizon keeps simulating between loading and saving (it needs a few seconds of # chunk generation first), so counters and positions of live things move on. r'^chunk\.block_entities\.\(2, -60, 5\)\.(cooking_time_spent|lit_time_remaining|Items\[\]\.count)$': @@ -464,6 +472,13 @@ def normalize_chunk(root): return out, sections +# ChunkStatus before NOISE: nothing has filled the chunk's blocks yet. +BLANK_STATUSES = {'minecraft:empty', 'minecraft:structure_starts', 'minecraft:structure_references', + 'minecraft:biomes'} +# ChunkStatus LIGHT and later: the chunk's light is saved. +LIT_STATUSES = {'minecraft:light', 'minecraft:spawn', 'minecraft:full'} + + def compare_chunks(vanilla, horizon, report): for key in sorted(set(vanilla) | set(horizon)): if key not in horizon: @@ -476,9 +491,10 @@ def compare_chunks(vanilla, horizon, report): ha, hs = normalize_chunk(horizon[key]) out = [] diff(va, ha, 'chunk', out) - if va.get('Status') != 'minecraft:full' and ha.get('Status') == 'minecraft:full': - promoted = re.compile(r'^chunk\.(Status|Heightmaps\.\w+_WG|entities)$') - out = [(path.replace('chunk.', 'chunk.promoted.', 1) if promoted.match(path) else path, p, q) + promoted = va.get('Status') != 'minecraft:full' and ha.get('Status') == 'minecraft:full' + if promoted: + rule = re.compile(r'^chunk\.(Status|Heightmaps\.\w+_WG|entities)$') + out = [(path.replace('chunk.', 'chunk.promoted.', 1) if rule.match(path) else path, p, q) for path, p, q in out] for y in sorted(set(vs) | set(hs)): empty = {'blocks': ['minecraft:air'] * 4096, 'biomes': None, 'BlockLight': None, 'SkyLight': None} @@ -493,6 +509,16 @@ def compare_chunks(vanilla, horizon, report): if a[light] != b[light]: out.append((f'chunk.sections.{light}[{y}]', None if a[light] is None else 'present', None if b[light] is None else 'present')) + # Before its terrain a chunk has no blocks, light or heightmaps yet. + if promoted and va.get('Status') in BLANK_STATUSES: + blank = re.compile(r'^chunk\.(Heightmaps|sections)\.') + out = [(path.replace('chunk.', 'chunk.blank.', 1) if blank.match(path) else path, p, q) + for path, p, q in out] + # Before its light step a chunk saves no light; Horizon lights what it finishes. + elif promoted and va.get('Status') not in LIT_STATUSES: + unlit = re.compile(r'^chunk\.sections\.(SkyLight|BlockLight)\[') + out = [(path.replace('chunk.', 'chunk.unlit.', 1) if unlit.match(path) else path, p, q) + for path, p, q in out] for path, p, q in out: report.append((f'{path} @{key}', p, q))