mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
On a server that doesn't offer horizon:lod (any vanilla, Paper or Spigot server), every chunk it sends is kept as its far-terrain summary, on disk per server, world and dimension (server-terrain/<server>/<hashed seed>/<namespace>/<level>/, Anvil region files), and drawn beyond the server's view distance on this visit and later ones: a server never sends its seed, so the client draws what it was sent. A chunk the client forgets (ForgetLevelChunk, or a moved or resized storage) is observed one last time first. A server's offer (horizon:lod) still comes first, and a friend's world keeps nothing (its code changes at every share), nor a splitscreen guest's (the player's own world, its other guests writing the same folder). The far terrain is rebuilt when the level changes; a proxy's next server comes through reconfiguration. Cells wider than 128 chunks aren't searched for kept chunks, and a failed write is logged once. tools/server_lod_e2e.py's server without an offer now expects these kept chunks (source "server-terrain"). server_terrain lives in horizon-client-render beside lod and streaming. The launcher leaves server-terrain/ out when it duplicates an instance: it is a cache, rebuilt as the instance plays. Co-Authored-By: Claude Opus 5.5 <[email protected]>
120 lines
4.8 KiB
Python
120 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Far terrain on a dedicated server (`horizon-lod-distance`, crates/horizon-client/src/lod_net.rs), end to end.
|
|
|
|
python3 tools/server_lod_e2e.py --bin-dir target/fast
|
|
|
|
A real Horizon client (horizon-ctl, offscreen) with LOD Distance 32 joins a dedicated server:
|
|
- `horizon-lod-distance=16`: the server samples the client's far terrain (`status`'s lod.source is "server"), no
|
|
farther than 16 chunks (effective_distance 16), and far tiles are drawn (active_tiles > 0);
|
|
- no `horizon-lod-distance` (off by default): the server offers none, so the client draws only the chunks it was
|
|
sent (source "server-terrain", `server_terrain.rs`), nothing the server sampled.
|
|
|
|
Each server and game runs in fresh folders with fresh HORIZON_HOME and XDG folders (lab.game_env), on a free
|
|
port. Build first:
|
|
cargo build --profile fast -p horizon-client -p horizon-client-lib --bin horizon-client --bin horizon-ctl
|
|
"""
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import lab # noqa: E402
|
|
from lan_e2e import Failed, Game # noqa: E402
|
|
from server_bench import DONE, Server # noqa: E402
|
|
|
|
EXE = '.exe' if os.name == 'nt' else ''
|
|
PROPERTIES = {
|
|
'online-mode': 'false',
|
|
'white-list': 'false',
|
|
'gamemode': 'creative',
|
|
'force-gamemode': 'true',
|
|
'view-distance': '4',
|
|
'simulation-distance': '4',
|
|
'spawn-protection': '0',
|
|
}
|
|
CLIENT_LOD = 32
|
|
|
|
|
|
def server(bins, folder, out, name, extra):
|
|
folder.mkdir(parents=True)
|
|
port = lab.free_port()
|
|
props = dict(PROPERTIES, **extra, **{'server-port': str(port)})
|
|
(folder / 'server.properties').write_text(''.join(f'{k}={v}\n' for k, v in props.items()))
|
|
(folder / 'eula.txt').write_text('eula=true\n')
|
|
command = [bins / f'horizon-client{EXE}', '--resources', lab.RESOURCES, '--dedicated', '--nogui']
|
|
s = Server(command, folder, out / f'{name}.log')
|
|
s.wait_for(DONE, 300)
|
|
return s, port
|
|
|
|
|
|
def far_terrain(bins, out, port, name, wanted, seconds):
|
|
"""Join with LOD Distance CLIENT_LOD; the state's lod part once `wanted` holds of it (or after `seconds`)."""
|
|
game = Game(name, bins / f'horizon-ctl{EXE}', lab.RESOURCES)
|
|
for folder in (game.work, game.work / 'home'):
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
(folder / 'horizon-graphics.txt').write_text(f'lodDistance:{CLIENT_LOD}\n')
|
|
try:
|
|
game.pid = game.raw('launch', '--offscreen', '--no-pause')['pid']
|
|
game.raw('connect', f'127.0.0.1:{port}', name)
|
|
game.raw('wait-for', 'loaded', '--timeout', 300)
|
|
deadline = time.time() + seconds
|
|
while True:
|
|
lod = game('status')['lod']
|
|
if wanted(lod) or time.time() > deadline:
|
|
break
|
|
game.wait(20)
|
|
game.shot(out, 'far')
|
|
return lod
|
|
finally:
|
|
(out / f'{name}-game.log').write_text(game.close())
|
|
shutil.rmtree(game.work, ignore_errors=True)
|
|
|
|
|
|
def check(bins, scratch, out):
|
|
s, port = server(bins, scratch / 'on', out, 'on', {'horizon-lod-distance': '16'})
|
|
try:
|
|
lod = far_terrain(bins, out, port, 'Far', lambda l: l['source'] == 'server' and l['active_tiles'] > 20, 90)
|
|
finally:
|
|
s.stop()
|
|
print(f'on: {lod}')
|
|
if lod['distance'] != CLIENT_LOD:
|
|
raise Failed(f'on: the client did not take LOD Distance {CLIENT_LOD}: {lod}')
|
|
if lod['source'] != 'server' or lod['effective_distance'] != 16:
|
|
raise Failed(f'on: far terrain is not the server\'s, 16 chunks: {lod}')
|
|
if lod['active_tiles'] <= 20:
|
|
raise Failed(f'on: no far tiles were drawn: {lod}')
|
|
print('on: the server samples far terrain, capped at its 16 chunks')
|
|
s, port = server(bins, scratch / 'off', out, 'off', {})
|
|
try:
|
|
lod = far_terrain(bins, out, port, 'Near', lambda l: l['source'] != 'none', 15)
|
|
finally:
|
|
s.stop()
|
|
print(f'off: {lod}')
|
|
if lod['source'] != 'server-terrain':
|
|
raise Failed(f'off: expected only the chunks the server sent (server-terrain): {lod}')
|
|
print('off: only the chunks the server sent')
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('--bin-dir', default=str(lab.ROOT / 'target/fast'))
|
|
parser.add_argument('--out', default=str(lab.LOCAL / 'server-lod-e2e'))
|
|
args = parser.parse_args()
|
|
bins, out = Path(args.bin_dir).resolve(), Path(args.out)
|
|
shutil.rmtree(out, ignore_errors=True)
|
|
out.mkdir(parents=True)
|
|
os.environ.update(lab.game_env())
|
|
with tempfile.TemporaryDirectory(prefix='server-lod-e2e-') as scratch:
|
|
try:
|
|
check(bins, Path(scratch), out)
|
|
except (Failed, RuntimeError, AssertionError) as e:
|
|
sys.exit(f'server_lod_e2e failed (logs in {out}): {e}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|