874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
163 lines
8.1 KiB
Python
163 lines
8.1 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 random
|
|
|
|
try:
|
|
from . import lab, oracle_harness as harness
|
|
except ImportError:
|
|
import lab, oracle_harness as harness
|
|
|
|
|
|
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 lab.fbits(mspt)])
|
|
elif kind == 'real':
|
|
ops.append(['real', now])
|
|
else:
|
|
ops.append([kind, rng.random() < 0.5])
|
|
yield dict(t='timer', tps=lab.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', lab.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=lab.dbits(rng.uniform(0, w)), y=lab.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', lab.dbits(x), lab.dbits(y), lab.dbits(rng.uniform(-30, 30) * big), lab.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', lab.dbits(time)])
|
|
else:
|
|
ops.append(['ignore'])
|
|
yield dict(t='mouse', sens=lab.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([lab.dbits(x), lab.dbits(y)])
|
|
case = dict(t='scroll', discrete=rng.random() < 0.4, sens=lab.dbits(rng.choice([1.0, 0.01, 10.0, 0.5, 2.0, 3.1622776601683795])),
|
|
slot=rng.randrange(9), ops=ops)
|
|
yield case
|
|
yield dict(case, spectator=True)
|
|
# Bounds, recovery from each bound, fractional accumulation, and horizontal-only input.
|
|
yield dict(t='scroll', discrete=False, sens=lab.dbits(1.0), slot=4, spectator=True,
|
|
ops=[[lab.dbits(x), lab.dbits(y)] for x, y in
|
|
[(0, 100), (0, -1), (0, -100), (0, 1), (0, .4), (0, .7),
|
|
(0, -.4), (0, -.7), (5, 0), (-5, 0)]])
|
|
|
|
|
|
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, hotbar selection and spectator flight speed, 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 / 'crates/horizon-client/src/clock.rs']
|
|
+ [lab.ROOT / f'crates/horizon-client-input/src/{name}.rs' for name in ('mouse', 'window', 'keymapping')])
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|