3ce74d1351
Brings the options, timer and mouse parity port on top of everything that landed on main (raids, scoreboards, warden, subtitles and more), keeping both sides in game.rs, clock.rs and settings.rs. The subtitle option test now expects vanilla's behaviour of keeping the default when notificationDisplayTime is out of range. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
165 lines
7.7 KiB
Python
165 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the game loop and input handling with the pinned client: the
|
|
DeltaTracker.Timer, ServerTickRateManager (freeze, step, sprint), Window
|
|
scale computation, FramerateLimitTracker decisions, MouseHandler (relative
|
|
and absolute motion, sensitivity curve, cinematic smoothing, inversion,
|
|
scroll accumulation and discrete scrolling), Entity.turn and ToggleKeyMapping,
|
|
all driven with identical synthetic input and time sequences."""
|
|
import json
|
|
import math
|
|
import random
|
|
import struct
|
|
|
|
try:
|
|
from . import lab, oracle_harness as harness
|
|
except ImportError:
|
|
import lab, oracle_harness as harness
|
|
|
|
|
|
def fbits(v):
|
|
return struct.pack('>f', v).hex()
|
|
|
|
|
|
def dbits(v):
|
|
return struct.pack('>d', v).hex()
|
|
|
|
|
|
def timer_cases(rng):
|
|
for n in range(700):
|
|
tps = rng.choice([20.0, 20.0, 10.0, 60.0, 1.0, 144.0])
|
|
start = rng.choice([0, 0, 1234, 10**9])
|
|
now = start
|
|
ops = []
|
|
for _ in range(rng.randrange(1, 40)):
|
|
kind = rng.choice(['adv', 'adv', 'adv', 'real', 'pause', 'frozen'])
|
|
if kind in ('adv', 'real'):
|
|
now += rng.choice([0, 1, 2, 7, 16, 17, 33, 49, 50, 51, 99, 100, 101, 250, 999, 5000, 70000])
|
|
if rng.random() < 0.03:
|
|
now -= rng.randrange(0, 30)
|
|
if kind == 'adv':
|
|
mspt = rng.choice([None, None, None, 50.0, 25.0, 100.0, 500.0, 1000.0, 0.5, 5.0, 0.1])
|
|
ops.append(['adv', now, None if mspt is None else fbits(mspt)])
|
|
elif kind == 'real':
|
|
ops.append(['real', now])
|
|
else:
|
|
ops.append([kind, rng.random() < 0.5])
|
|
yield dict(t='timer', tps=fbits(tps), start=start, ops=ops)
|
|
|
|
|
|
def rate_cases(rng):
|
|
rates = [20.0, 1.0, 0.5, 0.0, -3.0, 100.0, 10000.0, 3.3, 1e-3, 33.333, 7.0, float('nan'), float('inf'), 59.99]
|
|
for n in range(500):
|
|
ops = []
|
|
clock = 0
|
|
for _ in range(rng.randrange(1, 40)):
|
|
kind = rng.choice(['rate', 'freeze', 'step', 'stopstep', 'sprint', 'check', 'check', 'check', 'end', 'stopsprint',
|
|
'tick', 'tick', 'tick'])
|
|
if kind == 'rate':
|
|
ops.append(['rate', fbits(rng.choice(rates))])
|
|
elif kind == 'freeze':
|
|
ops.append(['freeze', rng.random() < 0.5])
|
|
elif kind == 'step':
|
|
ops.append(['step', rng.choice([1, 2, 5, 20, 0, -1])])
|
|
elif kind == 'sprint':
|
|
ops.append(['sprint', rng.choice([1, 2, 3, 10, 100, 0])])
|
|
elif kind in ('check', 'end'):
|
|
clock += rng.choice([0, 1, 1000, 50_000_000, 123_456_789, 3_000_000_000])
|
|
ops.append([kind, clock])
|
|
else:
|
|
ops.append([kind])
|
|
yield dict(t='rate', ops=ops)
|
|
|
|
|
|
def scale_cases(rng):
|
|
sizes = [(1, 1), (2, 2), (319, 239), (320, 240), (321, 241), (640, 480), (854, 480), (1280, 720), (1920, 1080),
|
|
(2560, 1440), (3840, 2160), (7680, 4320), (0, 0), (1000, 100), (100, 1000), (641, 481), (333, 777)]
|
|
for w, h in sizes:
|
|
for maxs in [0, 1, 2, 3, 4, 5, 6, 8, 100, -1, 7]:
|
|
for uni in (False, True):
|
|
sw, sh = rng.choice([(w, h), (max(w // 2, 1), max(h // 2, 1)), (w + 1, h + 3)])
|
|
yield dict(t='scale', w=w, h=h, max=maxs, uni=uni, sw=sw, sh=sh,
|
|
x=dbits(rng.uniform(0, w)), y=dbits(rng.uniform(0, h)))
|
|
|
|
|
|
def limit_cases(rng):
|
|
for iconified, focused in [(True, False), (True, True), (False, True)]:
|
|
for opt in (10, 30, 60, 120, 140, 260):
|
|
for afk in (False, True):
|
|
for ago in (0, 1000, 59000, 61000, 599000, 601000, 5_000_000):
|
|
for level in (False, True):
|
|
for screen in (False, True):
|
|
for overlay in (False, True):
|
|
yield dict(t='limit', iconified=iconified, focused=focused, opt=opt, afk=afk, ago=ago,
|
|
level=level, screen=screen, overlay=overlay)
|
|
|
|
|
|
def mouse_cases(rng):
|
|
for n in range(900):
|
|
sens = rng.choice([0.0, 0.5, 1.0, 0.25, 0.1, 0.75, 0.3333, rng.random()])
|
|
time = rng.choice([0.0, 10.0, 12345.678])
|
|
ops = []
|
|
for _ in range(rng.randrange(1, 40)):
|
|
kind = rng.choice(['move', 'move', 'move', 'frame', 'frame', 'ignore'])
|
|
if kind == 'move':
|
|
x, y = rng.uniform(0, 1920), rng.uniform(0, 1080)
|
|
big = rng.choice([1, 1, 1, 50, 5000])
|
|
ops.append(['move', dbits(x), dbits(y), dbits(rng.uniform(-30, 30) * big), dbits(rng.uniform(-30, 30) * big)])
|
|
elif kind == 'frame':
|
|
time += rng.choice([0.0, 0.001, 0.0069, 0.016, 0.0167, 0.05, 0.5, 2.0])
|
|
ops.append(['frame', dbits(time)])
|
|
else:
|
|
ops.append(['ignore'])
|
|
yield dict(t='mouse', sens=dbits(sens), smooth=rng.random() < 0.4, scoping=rng.random() < 0.3,
|
|
ix=rng.random() < 0.3, iy=rng.random() < 0.3, grabbed=rng.random() < 0.85, active=rng.random() < 0.9, ops=ops)
|
|
|
|
|
|
def scroll_cases(rng):
|
|
for n in range(700):
|
|
ops = []
|
|
for _ in range(rng.randrange(1, 40)):
|
|
x = rng.choice([0.0, 0.0, 0.0, 1.0, -1.0, 0.25, -0.25, 0.4, 2.0, 5.5, -7.0, rng.uniform(-3, 3)])
|
|
y = rng.choice([0.0, 1.0, -1.0, 1.0, -1.0, 0.1, -0.3, 0.5, -0.5, 2.0, -3.0, 120.0, rng.uniform(-3, 3)])
|
|
ops.append([dbits(x), dbits(y)])
|
|
yield dict(t='scroll', discrete=rng.random() < 0.4, sens=dbits(rng.choice([1.0, 0.01, 10.0, 0.5, 2.0, 3.1622776601683795])),
|
|
slot=rng.randrange(9), ops=ops)
|
|
|
|
|
|
def key_cases(rng):
|
|
for n in range(900):
|
|
toggle = rng.random() < 0.8
|
|
ops = []
|
|
for _ in range(rng.randrange(1, 50)):
|
|
ops.append(rng.choice([['option', rng.random() < 0.5], ['down', True], ['down', False], ['down', True], ['release'],
|
|
['restore'], ['click'], ['consume'], ['consume'], ['ingame']]))
|
|
yield dict(t='key', toggle=toggle, keyboard=rng.random() < 0.7, restore=rng.random() < 0.6, ops=ops)
|
|
|
|
|
|
def make_cases(build):
|
|
rng = random.Random(0x4c4f4f50)
|
|
path = build / 'cases.jsonl'
|
|
count = 0
|
|
with path.open('w') as out:
|
|
for generator in (timer_cases, rate_cases, scale_cases, limit_cases, mouse_cases, scroll_cases, key_cases):
|
|
for case in generator(rng):
|
|
out.write(json.dumps(case, separators=(',', ':')) + '\n')
|
|
count += 1
|
|
return path, count
|
|
|
|
|
|
def main():
|
|
scope = ('Real DeltaTracker.Timer (float partial ticks from integer milliseconds, pause residual, frozen partial tick, '
|
|
'real-time delta cap), ServerTickRateManager (set rate, freeze, step, sprint including the sprint report text), '
|
|
'Window.calculateScale/setGuiScale and MouseHandler scaled positions, FramerateLimitTracker throttle reasons and '
|
|
'limits (not the focus-lost exclusive-fullscreen branch, which reads SDL), MouseHandler.onMove/turnPlayer with '
|
|
'sensitivity, cinematic smoothing, scoping and inversion, Entity.turn clamps, onScroll with ScrollWheelHandler '
|
|
'accumulation, discrete scrolling and hotbar selection, and ToggleKeyMapping/KeyMapping down, click and '
|
|
'screen-release state. The frame clock source (Blaze3D.getTime, System.nanoTime) is injected, so sleeping in '
|
|
'FramerateLimiter and vsync present-mode selection are covered by Rust unit tests only.')
|
|
harness.differential(
|
|
'rust-game-loop-parity', 'VanillaGameLoopOracle', 'game_loop_probe', make_cases, scope,
|
|
sources=[lab.ROOT / f'crates/horizon-client/src/{name}.rs' for name in ('clock', 'mouse', 'window', 'keymapping')])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|