Files
horizon/tools/singleplayer_end_cargo_audit.py
rdbtCVSandClaude Opus 5.5 23f8bd4fed Remove the deprecated in-frame single-player path
Single player only runs on its server thread now. --in-frame and
--no-server-thread print a notice, and HORIZON_SERVER_THREAD is ignored.
This deletes the code that only the in-frame path reached: the window's
world loader and local player tick, its portal travel, the loading screen
and ring-by-ring streaming, and the in-frame LAN host (the host player in
the hub, its models of the guests, the reserved uuid). The world-upgrade
progress screen with its Cancel button moves to the server-thread start.
Tools, the harnesses and the docs no longer use the in-frame path.

Closes #6473

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 09:30:14 +00:00

143 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Check #1518: mobs and dropped items go through an End gateway.
Build horizon-client and horizon-ctl with the fast profile first, then run:
python3 tools/singleplayer_end_cargo_audit.py --case gateway
Runs singleplayer on its server thread: a cow and a seven-diamond stack stand in a gateway in the End, and both are
expected to leave it and land at the gateway's exit, the stack with its seven
diamonds.
"""
import argparse
import math
import os
from pathlib import Path
import re
import subprocess
import tempfile
import time
from agent_control_e2e import Cli
import lab
# The gateway beside the End's spawn platform, where the player waits.
GATEWAY = (104, 50, 0)
# The diamonds the dropped stack carries.
DIAMONDS = 7
def data_get(cli, selector, path):
"""The chat line of a `/data get entity`."""
since = cli('events')['last']
cli('command', command=f'data get entity {selector} {path}')
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
reply = cli('events', since=since)
for event in reply['events']:
text = event.get('text', '')
if event.get('event') == 'chat' and ('entity data' in text or 'No entity was found' in text):
return text
time.sleep(0.1)
raise AssertionError(f'no answer for /data get entity {selector} {path}')
def numbers(text):
"""The numbers of a `/data get` answer: "Pos: [104.5, 50.0, 0.5]"."""
return [float(v) for v in re.findall(r'-?\d+(?:\.\d+)?', text.split(':', 1)[-1])]
def run(bin_dir, resources):
with tempfile.TemporaryDirectory(prefix='hz-end-gateway-') as scratch:
work = Path(scratch)
env = lab.game_env({k: v for k, v in os.environ.items() if k not in (
'HORIZON_HOME', 'HORIZON_LAUNCHER_HOME', 'XDG_DATA_HOME', 'XDG_CONFIG_HOME')})
home = Path(env['HORIZON_HOME'])
socket = work / 'game.sock'
cli = Cli(bin_dir / 'horizon-ctl', socket, home, env)
args = [str(bin_dir / 'horizon-client'), '--resources', str(resources),
'--world', 'cargo', '--creative', '--seed', '4242', '--offscreen',
'--render-distance', '4', '--control', '--control-socket', str(socket)]
with (work / 'game.log').open('w') as log:
process = subprocess.Popen(args, cwd=home, env=env, stdout=log, stderr=log)
try:
deadline = time.monotonic() + 120
while not socket.exists():
if process.poll() is not None or time.monotonic() > deadline:
raise RuntimeError('game did not open its control socket')
time.sleep(0.1)
cli('wait_ready', seconds=240)
# The End, on its spawn platform.
cli('command', command='execute in minecraft:the_end run tp @s 100 50 0')
for _ in range(60):
cli('wait', ticks=20)
if cli('state').get('dimension') == 'minecraft:the_end':
break
else:
raise AssertionError('did not enter the End')
cli('wait_ready', seconds=240)
# A gateway beside the player, with a cow and a stack in it.
x, y, z = GATEWAY
cli('command', command=(
f'execute in minecraft:the_end run setblock {x} {y} {z} minecraft:end_gateway'))
cli('command', command=(
f'execute in minecraft:the_end run summon minecraft:cow {x}.5 {y}.5 {z}.5 {{NoAI:1b,Tags:["horizon_gateway_cow"]}}'))
nbt = '{Tags:["horizon_gateway_item"],Item:{id:"minecraft:diamond",count:' + str(DIAMONDS) + '}}'
cli('command', command=(
'execute in minecraft:the_end run summon minecraft:item '
+ f'{x}.5 {y}.5 {z}.5 ' + nbt))
# The gateway takes the stack, and the cow 40 ticks later
# (TheEndGatewayBlockEntity.triggerCooldown).
cli('wait', ticks=120)
# The exit is 1024 blocks away (findOrCreateValidTeleportPos): the
# player waits there, so the travellers are near enough to see.
length = math.hypot(x, z)
ex, ez = 1024 * x / length, 1024 * z / length
cli('command', command=f'execute in minecraft:the_end run tp @s {ex:.1f} 80 {ez:.1f}')
for _ in range(60):
cli('wait', ticks=20)
cow = data_get(cli, '@e[type=cow,tag=horizon_gateway_cow,limit=1]', 'Pos')
if 'entity data' in cow:
break
else:
raise AssertionError('the cow never arrived')
item = data_get(cli, '@e[type=item,tag=horizon_gateway_item,limit=1]', 'Pos')
count = numbers(data_get(cli, '@e[type=item,tag=horizon_gateway_item,limit=1]', 'Item.count'))
cow_at, item_at = numbers(cow), numbers(item)
print(f'cow at {cow_at}, item at {item_at}, count {count}')
assert len(cow_at) == 3, f'the cow has no position: {cow_at}'
assert len(item_at) == 3, f'the stack has no position: {item_at}'
for name, at in (('cow', cow_at), ('item', item_at)):
assert abs(at[0] - x) > 64 or abs(at[2] - z) > 64, \
f'{name} stayed in the gateway at {at}'
assert count and int(count[0]) == DIAMONDS, f'the stack lost diamonds: {count}'
cli('quit')
assert process.wait(timeout=120) == 0
except Exception:
log.flush()
text = (work / 'game.log').read_text(errors='replace')
Path('/tmp/end-gateway-game.log').write_text(text)
for log in (home / 'logs').glob('*'):
if log.is_file():
Path('/tmp/end-gateway-' + log.name).write_text(
log.read_text(errors='replace'))
print(text[-4000:])
raise
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=30)
print('PASS end gateway: the cow and the stack left the gateway with their data')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--case', choices=['gateway'], default='gateway')
parser.add_argument('--bin-dir', type=Path, default=lab.cargo_target() / 'fast')
parser.add_argument('--resources', type=Path, default=lab.RESOURCES)
args = parser.parse_args()
run(args.bin_dir.resolve(), args.resources.resolve())
if __name__ == '__main__':
main()