mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
The three harnesses add commands to the same LocatorBarClient lines; this branch now builds on #1847 (itself on #1843), keeps all three commands, and test_cursor_death_parity.py runs through locator.one_player. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
75951a769a
9 files changed
+256
-113
No files matched your search
@@ -1598,8 +1598,12 @@ impl Game<'_> {
|
||||
if self.screen != Screen::Dead {
|
||||
return Err("respawn needs the death screen".into());
|
||||
}
|
||||
let label =
|
||||
horizon_client::text::Text::translate("deathScreen.respawn", &[]).plain();
|
||||
let key = if self.level.hardcore {
|
||||
"deathScreen.spectate"
|
||||
} else {
|
||||
"deathScreen.respawn"
|
||||
};
|
||||
let label = horizon_client::text::Text::translate(key, &[]).plain();
|
||||
let w = widgets::find(&widgets::last(), &label)?;
|
||||
if !w.enabled {
|
||||
// DeathScreen enables its buttons after 20 ticks.
|
||||
|
||||
@@ -1694,7 +1694,26 @@ impl Game<'_> {
|
||||
targets: targets_guests,
|
||||
};
|
||||
let guest_entities = self.lan_guest_entities();
|
||||
// ThrownEnderpearl.tick: a dead player's pearl is discarded in
|
||||
// place of its tick while ender_pearls_vanish_on_death is on.
|
||||
let vanish = self.ui.rules.bool("ender_pearls_vanish_on_death");
|
||||
let host_dead = self.player_view().is_some_and(|p| !p.alive);
|
||||
let dead_guests: Vec<u32> = self.lan.as_ref().map_or_else(Vec::new, |h| {
|
||||
h.hub
|
||||
.guests
|
||||
.iter()
|
||||
.filter(|g| g.dead)
|
||||
.map(|g| g.conn)
|
||||
.collect()
|
||||
});
|
||||
projectiles.retain_mut(|p| {
|
||||
if vanish
|
||||
&& matches!(p.kind, ProjectileKind::EnderPearl)
|
||||
&& p.owner == Some(Who::Player)
|
||||
&& p.guest.map_or(host_dead, |c| dead_guests.contains(&c))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mob_shot = !matches!(p.owner, Some(Who::Player));
|
||||
// A player's arrows, snowballs and eggs meet every player; a
|
||||
// guest's flies from that guest (Projectile.getOwner), not
|
||||
|
||||
@@ -1670,6 +1670,11 @@ impl Game<'_> {
|
||||
border: &self.border,
|
||||
};
|
||||
host.hub.respawn(i, spawn, &ctx);
|
||||
// ServerGamePacketListenerImpl.handleClientCommand: a hardcore world's
|
||||
// respawned player only watches.
|
||||
if self.level.hardcore {
|
||||
host.hub.set_game_mode(conn, SPECTATOR);
|
||||
}
|
||||
if let Some(config) = elsewhere {
|
||||
self.respawn_trip(conn, config);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use super::{Game, Screen};
|
||||
use horizon_client::{
|
||||
Result,
|
||||
command::args::GameMode,
|
||||
hud::{HudBatch, rgba},
|
||||
text::{self, Text},
|
||||
ui,
|
||||
@@ -81,9 +82,15 @@ fn pause_icons(w: f32, h: f32) -> [(f32, f32, &'static str); 4] {
|
||||
(x0 + 72.0, y, "gui/sprites/pause_menu/player_reporting"),
|
||||
]
|
||||
}
|
||||
fn death_buttons(w: f32, h: f32) -> [(f32, f32, &'static str, u8); 2] {
|
||||
/// DeathScreen.init: a hardcore player can only spectate.
|
||||
fn death_buttons(w: f32, h: f32, hardcore: bool) -> [(f32, f32, &'static str, u8); 2] {
|
||||
let respawn = if hardcore {
|
||||
"deathScreen.spectate"
|
||||
} else {
|
||||
"deathScreen.respawn"
|
||||
};
|
||||
[
|
||||
(w / 2.0 - 100.0, h / 4.0 + 72.0, "deathScreen.respawn", 0),
|
||||
(w / 2.0 - 100.0, h / 4.0 + 72.0, respawn, 0),
|
||||
(
|
||||
w / 2.0 - 100.0,
|
||||
h / 4.0 + 96.0,
|
||||
@@ -223,7 +230,12 @@ impl Game<'_> {
|
||||
),
|
||||
);
|
||||
}
|
||||
let title = Text::translate("deathScreen.title", &[]);
|
||||
let title = if self.level.hardcore {
|
||||
"deathScreen.title.hardcore"
|
||||
} else {
|
||||
"deathScreen.title"
|
||||
};
|
||||
let title = Text::translate(title, &[]);
|
||||
let tw = atlas.font.width_text(&title);
|
||||
ui::text_scaled(
|
||||
batch,
|
||||
@@ -258,7 +270,7 @@ impl Game<'_> {
|
||||
.ui
|
||||
.died_at
|
||||
.is_none_or(|t| t.elapsed().as_millis() >= 1000);
|
||||
for (x, y, key, _) in death_buttons(w, h) {
|
||||
for (x, y, key, _) in death_buttons(w, h, self.level.hardcore) {
|
||||
let hover = active && ui::hovered(self.cursor, x, y, 200.0, 20.0);
|
||||
ui::button(
|
||||
batch,
|
||||
@@ -286,7 +298,7 @@ impl Game<'_> {
|
||||
.as_ref()
|
||||
.map_or([1, 1], |r| [r.config.width, r.config.height]);
|
||||
let b = HudBatch::new(size);
|
||||
let hit = death_buttons(b.width, b.height)
|
||||
let hit = death_buttons(b.width, b.height, self.level.hardcore)
|
||||
.into_iter()
|
||||
.find(|d| ui::hovered(self.cursor, d.0, d.1, 200.0, 20.0))
|
||||
.map(|d| d.3);
|
||||
@@ -330,6 +342,11 @@ impl Game<'_> {
|
||||
Some(1) => self.close_world()?,
|
||||
_ => {}
|
||||
}
|
||||
// ServerGamePacketListenerImpl.handleClientCommand: a hardcore world's
|
||||
// respawned player only watches.
|
||||
if hit == Some(0) && self.remote.is_none() && self.level.hardcore {
|
||||
self.set_mode(GameMode::Spectator);
|
||||
}
|
||||
if hit.is_some() {
|
||||
self.audio.play_button_click();
|
||||
}
|
||||
|
||||
@@ -86,8 +86,10 @@ import net.minecraft.world.phys.Vec3;
|
||||
* handled everything this player sent before, and everything the server sent
|
||||
* this player before the reply has been recorded), {@code respawn NAME} (the
|
||||
* death screen's button, ServerboundClientCommandPacket PERFORM_RESPAWN),
|
||||
* {@code pickup NAME SLOT} (a left click on that slot of the player's
|
||||
* inventory menu, claiming nothing), {@code quit NAME}, {@code
|
||||
* {@code use NAME PITCH} (the main hand's item used looking that way,
|
||||
* ServerboundUseItemPacket), {@code pickup NAME SLOT} (a left click on that
|
||||
* slot of the player's inventory menu, claiming nothing), {@code quit NAME},
|
||||
* {@code
|
||||
* players on|off} (whether each player also records the other players added
|
||||
* to and removed from its level, and its own respawns into a dimension),
|
||||
* {@code chat on|off} (whether the system chat each player receives is
|
||||
@@ -135,6 +137,7 @@ public final class LocatorBarClient {
|
||||
case "still" -> players.get(w[1]).still();
|
||||
case "respawn" -> players.get(w[1]).send(new net.minecraft.network.protocol.game.ServerboundClientCommandPacket(
|
||||
net.minecraft.network.protocol.game.ServerboundClientCommandPacket.Action.PERFORM_RESPAWN));
|
||||
case "use" -> players.get(w[1]).use(Float.parseFloat(w[2]));
|
||||
case "pickup" -> players.get(w[1]).send(new net.minecraft.network.protocol.game.ServerboundContainerClickPacket(
|
||||
0, 0, Short.parseShort(w[2]), (byte) 0, net.minecraft.world.inventory.ContainerInput.PICKUP,
|
||||
new it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap<>(), net.minecraft.network.HashedStack.EMPTY));
|
||||
@@ -258,6 +261,13 @@ public final class LocatorBarClient {
|
||||
}
|
||||
}
|
||||
|
||||
private int sequence;
|
||||
|
||||
void use(float pitch) throws Exception {
|
||||
send(new net.minecraft.network.protocol.game.ServerboundUseItemPacket(
|
||||
net.minecraft.world.InteractionHand.MAIN_HAND, ++sequence, position.yRot(), pitch));
|
||||
}
|
||||
|
||||
void still() throws Exception {
|
||||
PositionMoveRotation at = position;
|
||||
// A tick of its own: the server takes one position per client tick (receivedPositionThisTick).
|
||||
|
||||
@@ -10,23 +10,12 @@ inventoryMenu.removed drops the cursor's stack where it died. Alice then asks wh
|
||||
site; the reply, and the inventory slots, respawns and teleports she is sent from the death on, are compared
|
||||
exactly.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
from . import lab, test_rust_parity as base, test_locator_bar_parity as locator
|
||||
from .test_dedicated_server_parity import Server
|
||||
from . import test_locator_bar_parity as locator
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
import test_locator_bar_parity as locator
|
||||
from test_dedicated_server_parity import Server
|
||||
|
||||
NAME = 'cursor-death-parity'
|
||||
# server_dir and generate_world write these server.properties.
|
||||
@@ -38,102 +27,28 @@ SCRIPT = [
|
||||
('pickup', ['pickup Alice 36']),
|
||||
('death', ['cmd Alice kill @s']),
|
||||
('respawn', ['respawn Alice']),
|
||||
('check', ['chat Alice on',
|
||||
'cmd Alice execute if entity @e[type=minecraft:item,x=10.5,y=-60,z=0.5,distance=..4]']),
|
||||
('check', ['chat on', 'cmd Alice execute if entity @e[type=minecraft:item,x=10.5,y=-60,z=0.5,distance=..4]']),
|
||||
]
|
||||
STEP_SECONDS = 4.0
|
||||
SHOWN = ('slot', 'inventory', 'respawn', 'teleport', 'chat')
|
||||
|
||||
|
||||
def run_side(label, server_command, client_command, build, world):
|
||||
directory = build / label
|
||||
port = locator.server_dir(directory, world)
|
||||
out = build / f'{label}.txt'
|
||||
out.unlink(missing_ok=True)
|
||||
server = Server(server_command, directory, label)
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
with (build / f'{label}-client.log').open('w') as log:
|
||||
client = subprocess.Popen([*map(str, client_command), '127.0.0.1', str(port), str(out)],
|
||||
cwd=lab.ROOT, stdin=subprocess.PIPE, stdout=log, stderr=subprocess.STDOUT,
|
||||
text=True)
|
||||
|
||||
def tell(line):
|
||||
client.stdin.write(line + '\n')
|
||||
client.stdin.flush()
|
||||
try:
|
||||
tell('players on')
|
||||
tell('state on')
|
||||
tell('step joins')
|
||||
tell('join Alice')
|
||||
server.wait_for(r'Alice joined the game', timeout=120)
|
||||
time.sleep(STEP_SECONDS)
|
||||
for step, actions in SCRIPT:
|
||||
tell(f'step {step}')
|
||||
for action in actions:
|
||||
tell(action.replace('chat Alice', 'chat'))
|
||||
time.sleep(STEP_SECONDS)
|
||||
tell('end')
|
||||
client.wait(timeout=60)
|
||||
finally:
|
||||
if client.poll() is None:
|
||||
client.kill()
|
||||
finally:
|
||||
server.stop()
|
||||
if not out.exists():
|
||||
raise AssertionError(f'{label}: the client wrote no transcript (see {build}/{label}-client.log)')
|
||||
return out.read_text(encoding='utf-8').splitlines()
|
||||
|
||||
|
||||
def shown(lines):
|
||||
"""Alice's slots, respawns, teleports and chat from the death on."""
|
||||
steps = {'step=death', 'step=respawn', 'step=check'}
|
||||
return [line for line in lines if len(parts := line.split(' ', 3)) >= 3 and parts[1] in steps
|
||||
and parts[2] in SHOWN]
|
||||
return [line for line in lines if len(parts := line.split(' ', 3)) >= 3
|
||||
and parts[1] in ('step=death', 'step=respawn', 'step=check')
|
||||
and parts[2] in ('slot', 'inventory', 'respawn', 'teleport', 'chat')]
|
||||
|
||||
|
||||
def check(vanilla, _):
|
||||
if not any(l.startswith('Alice step=check chat') for l in vanilla):
|
||||
return 'vanilla answered no check command'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
build = lab.LOCAL / NAME
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java'
|
||||
classes = build / 'classes'
|
||||
shutil.rmtree(classes, ignore_errors=True)
|
||||
classes.mkdir()
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
subprocess.run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-nowarn', '-sourcepath',
|
||||
lab.ROOT / 'mods/horizon/src/test/java', '-cp', dependencies, '-d', classes, oracle],
|
||||
cwd=lab.ROOT, check=True, capture_output=True)
|
||||
subprocess.run(['cargo', 'build', '--release', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
|
||||
check=True, capture_output=True)
|
||||
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'],
|
||||
cwd=lab.ROOT, text=True))
|
||||
binary = Path(metadata['target_directory']) / 'release' / ('horizon-client.exe' if os.name == 'nt' else 'horizon-client')
|
||||
vanilla_server = ['java', '-Xmx2g', '-Duser.timezone=UTC', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
|
||||
horizon_server = [binary, '--resources', lab.LOCAL / 'horizon-client/26.3', '--dedicated', '--nogui']
|
||||
client = ['java', '-Xmx1g', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.LocatorBarClient']
|
||||
|
||||
world = locator.generate_world(vanilla_server, build)
|
||||
vanilla_all = run_side('vanilla', vanilla_server, client, build, world)
|
||||
horizon_all = run_side('horizon', horizon_server, client, build, world)
|
||||
errors = [l for l in vanilla_all + horizon_all if ' error ' in f' {l} ' or 'disconnected' in l]
|
||||
vanilla, horizon = shown(vanilla_all), shown(horizon_all)
|
||||
if errors or vanilla != horizon:
|
||||
import difflib
|
||||
diff = '\n'.join(difflib.unified_diff(vanilla, horizon, 'vanilla', 'horizon', n=1, lineterm=''))
|
||||
raise AssertionError(f'{len(errors)} errors, transcripts differ (see {build}/vanilla.txt and horizon.txt):\n'
|
||||
+ '\n'.join(errors[:10]) + '\n' + diff[:6000])
|
||||
if not any(l.startswith('Alice step=check chat') for l in vanilla):
|
||||
raise AssertionError(f'vanilla answered no check command: the script did not run (see {build})')
|
||||
lab.write_json(report, dict(
|
||||
schema=1, minecraft=lock['minecraft']['version'], platform=platform.platform(),
|
||||
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
|
||||
lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'),
|
||||
scope='A player dying with a stack on the inventory menu cursor (keep_inventory off) and respawning on a '
|
||||
'dedicated server: the stack is dropped where it died, not kept.'))
|
||||
print(f'cursor death: {len(vanilla)} lines identical')
|
||||
lines = locator.one_player(NAME, SCRIPT, shown, check, 'A player dying with a stack on the inventory menu cursor '
|
||||
'(keep_inventory off) and respawning on a dedicated server: the stack is dropped '
|
||||
'where it died, not kept.')
|
||||
print(f'cursor death: {len(lines)} lines identical')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Respawning in a hardcore world on a dedicated server, against vanilla's, seen by a vanilla client.
|
||||
|
||||
A vanilla dedicated server and Horizon's (`horizon-client --dedicated --nogui`) each serve a copy of one hardcore
|
||||
flat world vanilla generated first, with Alice an operator in survival. LocatorBarClient (a player made of
|
||||
vanilla's protocol code) joins, runs /kill on itself and presses the death screen's button
|
||||
(ServerboundClientCommandPacket PERFORM_RESPAWN). Vanilla's ServerGamePacketListenerImpl.handleClientCommand
|
||||
respawns the player, then makes it a spectator because the server is hardcore. The game mode changes and respawns
|
||||
Alice is sent in the death and respawn steps are compared exactly.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from . import test_locator_bar_parity as locator
|
||||
except ImportError:
|
||||
import test_locator_bar_parity as locator
|
||||
|
||||
NAME = 'hardcore-respawn-parity'
|
||||
# server_dir and generate_world write these server.properties.
|
||||
locator.PROPERTIES = dict(locator.PROPERTIES, hardcore='true', gamemode='survival')
|
||||
SCRIPT = [
|
||||
('death', ['cmd Alice kill @s']),
|
||||
('respawn', ['respawn Alice']),
|
||||
]
|
||||
|
||||
|
||||
def shown(lines):
|
||||
"""Alice's game mode changes and respawns in the death and respawn steps."""
|
||||
return [line for line in lines if len(parts := line.split(' ', 3)) >= 3
|
||||
and parts[1] in ('step=death', 'step=respawn') and parts[2] in ('gamemode', 'respawn')]
|
||||
|
||||
|
||||
def check(vanilla, _):
|
||||
if 'Alice step=respawn gamemode 3' not in vanilla:
|
||||
return 'vanilla did not make the respawned player a spectator'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lines = locator.one_player(NAME, SCRIPT, shown, check, 'A player dying and respawning in a hardcore world on a '
|
||||
'dedicated server: the respawn and the game mode it is sent (spectator).')
|
||||
print(f'hardcore respawn: {len(lines)} lines identical')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except AssertionError as error:
|
||||
sys.exit(f'{NAME}: {error}')
|
||||
@@ -205,12 +205,13 @@ def run_side(label, server_command, client_command, build, world):
|
||||
return out.read_text(encoding='utf-8').splitlines()
|
||||
|
||||
|
||||
def main():
|
||||
def sides(name):
|
||||
"""The vanilla and Horizon dedicated servers and the LocatorBarClient that joins them, built for harness
|
||||
`name`: (lock, build directory, vanilla server, Horizon server, client) commands."""
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
build = lab.LOCAL / NAME
|
||||
build = lab.LOCAL / name
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
(build / 'report.json').unlink(missing_ok=True)
|
||||
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/LocatorBarClient.java'
|
||||
classes = build / 'classes'
|
||||
shutil.rmtree(classes, ignore_errors=True)
|
||||
@@ -228,7 +229,74 @@ def main():
|
||||
vanilla_server = ['java', '-Xmx2g', '-Duser.timezone=UTC', '-cp', dependencies, 'net.minecraft.server.Main', 'nogui']
|
||||
horizon_server = [binary, '--resources', lab.LOCAL / 'horizon-client/26.3', '--dedicated', '--nogui']
|
||||
client = ['java', '-Xmx1g', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.LocatorBarClient']
|
||||
return lock, build, vanilla_server, horizon_server, client
|
||||
|
||||
|
||||
def one_player(name, script, shown, check, scope, step_seconds=4.0):
|
||||
"""A harness of one player, Alice: on each server she joins, then runs `script`, a list of (step, client
|
||||
actions) `step_seconds` apart. The lines `shown(transcript)` keeps must match vanilla's exactly, and
|
||||
`check(vanilla's shown lines, vanilla's transcript)` returns why vanilla's run is no proof (the script did
|
||||
not run), or None."""
|
||||
lock, build, vanilla_server, horizon_server, client = sides(name)
|
||||
|
||||
def run(label, server_command, world):
|
||||
directory = build / label
|
||||
port = server_dir(directory, world)
|
||||
out = build / f'{label}.txt'
|
||||
out.unlink(missing_ok=True)
|
||||
server = Server(server_command, directory, label)
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
with (build / f'{label}-client.log').open('w') as log:
|
||||
process = subprocess.Popen([*map(str, client), '127.0.0.1', str(port), str(out)], cwd=lab.ROOT,
|
||||
stdin=subprocess.PIPE, stdout=log, stderr=subprocess.STDOUT, text=True)
|
||||
|
||||
def tell(line):
|
||||
process.stdin.write(line + '\n')
|
||||
process.stdin.flush()
|
||||
try:
|
||||
for line in ['players on', 'state on', 'step joins', 'join Alice']:
|
||||
tell(line)
|
||||
server.wait_for(r'Alice joined the game', timeout=120)
|
||||
time.sleep(step_seconds)
|
||||
for step, actions in script:
|
||||
tell(f'step {step}')
|
||||
for action in actions:
|
||||
tell(action)
|
||||
time.sleep(step_seconds)
|
||||
tell('end')
|
||||
process.wait(timeout=60)
|
||||
finally:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
finally:
|
||||
server.stop()
|
||||
if not out.exists():
|
||||
raise AssertionError(f'{label}: the client wrote no transcript (see {build}/{label}-client.log)')
|
||||
return out.read_text(encoding='utf-8').splitlines()
|
||||
|
||||
world = generate_world(vanilla_server, build)
|
||||
vanilla_all = run('vanilla', vanilla_server, world)
|
||||
horizon_all = run('horizon', horizon_server, world)
|
||||
errors = [l for l in vanilla_all + horizon_all if ' error ' in f' {l} ' or 'disconnected' in l]
|
||||
vanilla, horizon = shown(vanilla_all), shown(horizon_all)
|
||||
if errors or vanilla != horizon:
|
||||
import difflib
|
||||
diff = '\n'.join(difflib.unified_diff(vanilla, horizon, 'vanilla', 'horizon', n=1, lineterm=''))
|
||||
raise AssertionError(f'{len(errors)} errors, transcripts differ (see {build}/vanilla.txt and horizon.txt):\n'
|
||||
+ '\n'.join(errors[:10]) + '\n' + diff[:6000])
|
||||
if (why := check(vanilla, vanilla_all)) is not None:
|
||||
raise AssertionError(f'{why}: the script did not run (see {build})')
|
||||
lab.write_json(build / 'report.json', dict(
|
||||
schema=1, minecraft=lock['minecraft']['version'], platform=platform.platform(),
|
||||
java=subprocess.check_output(['java', '--version'], text=True).splitlines()[0],
|
||||
lines=len(vanilla), output_sha256=lab.digest(build / 'horizon.txt', 'sha256'), scope=scope))
|
||||
return vanilla
|
||||
|
||||
|
||||
def main():
|
||||
lock, build, vanilla_server, horizon_server, client = sides(NAME)
|
||||
report = build / 'report.json'
|
||||
world = generate_world(vanilla_server, build)
|
||||
vanilla = run_side('vanilla', vanilla_server, client, build, world)
|
||||
horizon = run_side('horizon', horizon_server, client, build, world)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""An ender pearl in flight when its thrower dies, on a dedicated server, against vanilla's, seen by a vanilla client.
|
||||
|
||||
A vanilla dedicated server and Horizon's (`horizon-client --dedicated --nogui`) each serve a copy of one flat world
|
||||
vanilla generated first, with Alice an operator in survival. LocatorBarClient (a player made of vanilla's protocol
|
||||
code) throws an ender pearl straight up (ServerboundUseItemPacket), runs /kill on itself while the pearl flies,
|
||||
presses the death screen's button (PERFORM_RESPAWN) and waits for the pearl to come down, with the rule
|
||||
ender_pearls_vanish_on_death true: ThrownEnderpearl.tick discards a dead owner's pearl, so the respawned player is
|
||||
never teleported. The teleports, game mode changes and respawns Alice is sent from the throw on are compared
|
||||
exactly.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from . import test_locator_bar_parity as locator
|
||||
except ImportError:
|
||||
import test_locator_bar_parity as locator
|
||||
|
||||
NAME = 'pearl-death-parity'
|
||||
# server_dir and generate_world write these server.properties.
|
||||
locator.PROPERTIES = dict(locator.PROPERTIES, gamemode='survival')
|
||||
SCRIPT = [
|
||||
('setup', ['cmd Alice gamerule ender_pearls_vanish_on_death true', 'cmd Alice give @s minecraft:ender_pearl']),
|
||||
# Straight up: it comes down about four seconds later, near where it was thrown.
|
||||
('throw', ['use Alice -90', 'cmd Alice kill @s']),
|
||||
('respawn', ['respawn Alice']),
|
||||
('landed', []),
|
||||
]
|
||||
|
||||
|
||||
def shown(lines):
|
||||
"""Alice's teleports, game mode changes and respawns from the throw on."""
|
||||
steps = {f'step={step}' for step, _ in SCRIPT if step != 'setup'}
|
||||
return [line for line in lines if len(parts := line.split(' ', 3)) >= 3 and parts[1] in steps
|
||||
and parts[2] in ('teleport', 'gamemode', 'respawn')]
|
||||
|
||||
|
||||
def check(vanilla, vanilla_all):
|
||||
killed = any(l.startswith('Alice step=throw killed') for l in vanilla_all)
|
||||
if not killed or 'Alice step=respawn respawn minecraft:overworld' not in vanilla:
|
||||
return 'vanilla did not kill and respawn Alice'
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
lines = locator.one_player(NAME, SCRIPT, shown, check, 'An ender pearl in flight when its thrower dies and '
|
||||
'respawns on a dedicated server, with ender_pearls_vanish_on_death true: the pearl '
|
||||
'vanishes and never teleports the player.')
|
||||
print(f'pearl death: {len(lines)} lines identical')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except AssertionError as error:
|
||||
sys.exit(f'{NAME}: {error}')
|
||||
Reference in new issue
Block a user