End: no exit portal while nobody is in the fight
With force-loaded levels ticking without players (#1525), Horizon placed the exit portal as soon as the End's main island was loaded. Vanilla only places it from scanState and the dragon's death, both with players in the fight, so a pre-generated End has no portal until someone arrives. test_forced_end_parity force-loads the End's arena on vanilla's and Horizon's dedicated servers with nobody online and compares what is around 0, 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2598,7 +2598,9 @@ impl Game<'_> {
|
||||
}
|
||||
pub(super) fn fight_ready(&mut self) -> bool {
|
||||
self.load_fight();
|
||||
self.fight.loaded
|
||||
// spawnExitPortal runs from scanState and the dragon's death, both
|
||||
// only with players in the fight.
|
||||
self.fight.loaded && self.fight.players
|
||||
}
|
||||
pub(super) fn exit_portal(&self) -> Option<[i32; 3]> {
|
||||
self.fight.exit_portal
|
||||
|
||||
@@ -46,7 +46,7 @@ file selects ~103.
|
||||
| 29 | Title, worlds, startup (49) | `L/title.rs` + `L/title/`, `G/startup_menu.rs`, `L/title_edition.rs` | world_options; `cargo test -p horizon-client --lib title` |
|
||||
| 30 | Dedicated server, horizon-server (48) | `G/dedicated.rs`, `G/dedicated/status.rs`, `crates/horizon-server/`, `crates/horizon-server-files/` | dedicated_server, server_files, user_cache, online_mode, server_ops_feedback |
|
||||
| 31 | Particles and client effects (46) | `L/particles.rs`, `G/render_tick.rs`, `G/level_particles.rs`, `L/animate_tick.rs` | particle_providers, block_particles, entity_particles, emitter_particles, marker_particles |
|
||||
| 32 | Bosses and the End (45) | `G/dragon.rs`, `G/wither.rs`, `G/end.rs` | server_dragon, end_podium, end_gateway |
|
||||
| 32 | Bosses and the End (45) | `G/dragon.rs`, `G/wither.rs`, `G/end.rs` | server_dragon, forced_end, end_podium, end_gateway |
|
||||
| 33 | Protocol and packets (44) | `crates/horizon-protocol/src/` (`packets/play.rs`, `entity_data.rs`, `connection.rs`) | dedicated_server, online_mode, secure_chat; `cargo test -p horizon-protocol` |
|
||||
| 34 | Multiplayer and LAN screens, friends (44) | `L/multiplayer.rs`, `G/lan_screen.rs`, `G/friends.rs`, `L/friends_screen.rs`, `crates/horizon-friends/`, `crates/horizon-relay/` | lan, world_options; `python3 tools/test_friends.py` |
|
||||
| 35 | Dispensers (43) | `G/dispenser.rs`, `G/dispense_fixture.rs` (the harness side) | dispenser |
|
||||
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The End of a dedicated server nobody has entered, force-loaded from the console, against vanilla's.
|
||||
|
||||
A vanilla dedicated server and Horizon's each open a copy of one world vanilla generated (its End not made yet).
|
||||
With no player online, the console force-loads the End's main island and its arena. A FORCED ticket keeps its level active
|
||||
(`ServerChunkCache.hasActiveTickets`, `TicketType.FORCED`), so the chunks are generated, loaded and ticked
|
||||
without a player in the dimension. With nobody in the fight, `EnderDragonFight.tick` neither scans the state nor
|
||||
places the exit portal: no bedrock and no torches around 0, 0. Both servers must answer the same.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
from . import lab, test_rust_parity as base
|
||||
from .test_dedicated_server_parity import Server, wait_ticks
|
||||
from .test_locator_bar_parity import generate_world, server_dir
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
from test_dedicated_server_parity import Server, wait_ticks
|
||||
from test_locator_bar_parity import generate_world, server_dir
|
||||
|
||||
NAME = 'forced-end-parity'
|
||||
END = 'execute in minecraft:the_end'
|
||||
QUERIES = [
|
||||
f'{END} run fill -3 0 -3 3 255 3 minecraft:air replace minecraft:bedrock',
|
||||
f'{END} run fill -3 0 -3 3 255 3 minecraft:air replace minecraft:wall_torch',
|
||||
]
|
||||
|
||||
|
||||
def answer(server, command, n):
|
||||
start = len(server.lines)
|
||||
server.console(command)
|
||||
server.console(f'say done {n}')
|
||||
server.wait_for(rf'\[Server\] done {n}$', timeout=120)
|
||||
replies = [l.split(']: ', 1)[-1] for l in server.lines[start:] if 'done ' not in l]
|
||||
return ' / '.join(r for r in replies if r) or '(no reply)'
|
||||
|
||||
|
||||
def run_side(label, command, build, world):
|
||||
directory = build / label
|
||||
server_dir(directory, world)
|
||||
server = Server(command, directory, label)
|
||||
try:
|
||||
server.wait_for(r'Done \(')
|
||||
# The main island's arena (EnderDragonFight.isArenaLoaded: chunks -8..8), in pieces of at most 256 chunks.
|
||||
for area in ('-128 -128 143 -1', '-128 0 143 143'):
|
||||
server.console(f'{END} run forceload add {area}')
|
||||
deadline = time.time() + 600
|
||||
for poll in range(1_000_000):
|
||||
reply = answer(server, f'{END} if loaded 0 0 0', f'loaded {poll}')
|
||||
if 'passed' in reply:
|
||||
break
|
||||
if time.time() > deadline:
|
||||
raise AssertionError(f'{label}: the End centre never loaded')
|
||||
time.sleep(2)
|
||||
wait_ticks(server, 200)
|
||||
return {q: answer(server, q, i) for i, q in enumerate(QUERIES)}
|
||||
finally:
|
||||
server.stop()
|
||||
|
||||
|
||||
def main():
|
||||
_, jars, libraries = base.verify_artifacts()
|
||||
build = lab.LOCAL / NAME
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
subprocess.run(['cargo', 'build', '--release', '-p', 'horizon-client', '--bin', 'horizon-client'], cwd=lab.ROOT,
|
||||
check=True, capture_output=True)
|
||||
binary = lab.cargo_target() / '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']
|
||||
world = generate_world(vanilla_server, build)
|
||||
vanilla = run_side('vanilla', vanilla_server, build, world)
|
||||
horizon = run_side('horizon', horizon_server, build, world)
|
||||
normal = lambda s: re.sub(r'\s+', ' ', s).strip()
|
||||
problems = [f'{q}\n vanilla: {vanilla[q]}\n horizon: {horizon[q]}'
|
||||
for q in QUERIES if normal(vanilla[q]) != normal(horizon[q])]
|
||||
if problems:
|
||||
raise AssertionError('the forced End differs:\n' + '\n'.join(problems))
|
||||
print(f'forced End: loaded and ticked with nobody in it, no exit portal, as vanilla ({vanilla[QUERIES[0]]})')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except AssertionError as error:
|
||||
sys.exit(f'{NAME}: {error}')
|
||||
Reference in New Issue
Block a user