874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
191 lines
7.5 KiB
Python
191 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the local player's ambient sounds with the vanilla 26.3 client.
|
|
|
|
The oracle runs the real `LocalPlayer.updateIsUnderwater` (enter/exit sounds
|
|
seeded from the level random, the `UnderLiquidAmbientSoundInstance`),
|
|
`UnderwaterAmbientSoundHandler`, `BubbleColumnAmbientSoundHandler` and
|
|
`BiomeAmbientSoundsHandler` over the real
|
|
`SoundEngine` (see VanillaSoundRig), then `SoundManager.tick`, once per client
|
|
tick. The `AMBIENT_SOUNDS` value (loops, moods and additions from the biomes
|
|
and dimensions of the data pack plus faster custom moods), the player's
|
|
position, eye height, eyes in water, spectator and removed flags and the
|
|
level's brightness are script inputs. Every step prints the level random's
|
|
next long (so every draw is compared), moodiness, the underwater state and
|
|
delay, the handler's loop instances (fade, direction, stopped, active), each
|
|
playing instance and the subtitle notifications. Horizon's `Ambience` and
|
|
`SoundEngine` replay the scripts.
|
|
"""
|
|
import json
|
|
import random
|
|
|
|
try:
|
|
from . import audio_harness as h, lab
|
|
except ImportError:
|
|
import audio_harness as h
|
|
import lab
|
|
|
|
|
|
def settings():
|
|
"""Every AMBIENT_SOUNDS value in the data pack."""
|
|
data = h.RESOURCES / 'data/minecraft'
|
|
values = []
|
|
for path in sorted(list((data / 'worldgen/biome').glob('*.json')) + list((data / 'dimension_type').glob('*.json'))):
|
|
v = json.loads(path.read_text()).get('attributes', {}).get('minecraft:audio/ambient_sounds')
|
|
if v is None:
|
|
continue
|
|
additions = v.get('additions', [])
|
|
if isinstance(additions, dict):
|
|
additions = [additions]
|
|
mood = v.get('mood')
|
|
values.append(dict(loop=v.get('loop'), mood=None if mood is None else (mood['sound'], mood['tick_delay'], mood['block_search_extent'], mood['offset']),
|
|
additions=[(a['sound'], a['tick_chance']) for a in additions]))
|
|
return values
|
|
|
|
|
|
class Script:
|
|
def __init__(self):
|
|
self.lines = []
|
|
|
|
def add(self, *parts):
|
|
self.lines.append(' '.join(map(str, parts)))
|
|
|
|
def sounds(self, value):
|
|
mood = '-' if value['mood'] is None else '{},{},{},{}'.format(value['mood'][0], value['mood'][1], value['mood'][2], lab.dbits(value['mood'][3]))
|
|
self.add('sounds', value['loop'] or '-', mood, *[f'{s},{lab.dbits(c)}' for s, c in value['additions']])
|
|
|
|
def player(self, at=(0.5, 64.0, 0.5), eye=1.62, water=0, spectator=0, removed=0, mode=0, height=1.8, columns=()):
|
|
self.add('player', *map(lab.dbits, at), h.bits(eye), water, spectator, removed, mode, h.bits(height),
|
|
*[f'{x},{y},{z},{d}' for x, y, z, d in columns])
|
|
|
|
def tick(self, n=1):
|
|
for _ in range(n):
|
|
self.add('tick')
|
|
|
|
|
|
def scripted(s, values):
|
|
by_loop = [v for v in values if v['loop']]
|
|
cave = next(v for v in values if v['mood'] and v['mood'][0] == 'minecraft:ambient.cave' and not v['loop'])
|
|
s.add('start', 1)
|
|
s.sounds(cave)
|
|
s.player(mode=1)
|
|
s.tick(6100)
|
|
s.player((100.25, -20.0, -7.75), mode=2)
|
|
s.tick(400)
|
|
s.player((3.5, 12.0, 3.5), mode=3)
|
|
s.tick(400)
|
|
# Biome loops cross-fading, back and forth.
|
|
s.add('start', 2)
|
|
for v in by_loop:
|
|
s.sounds(v)
|
|
s.tick(30)
|
|
s.sounds(by_loop[0])
|
|
s.tick(20)
|
|
s.sounds(by_loop[1])
|
|
s.tick(5)
|
|
s.sounds(by_loop[0])
|
|
s.tick(60)
|
|
s.sounds(dict(loop=None, mood=None, additions=[]))
|
|
s.tick(60)
|
|
# Water: enter, the loop's fade, sub-sounds, exit, re-entering mid-fade, spectators.
|
|
s.add('start', 3)
|
|
s.sounds(cave)
|
|
s.player(water=1, mode=0)
|
|
s.tick(300)
|
|
s.player(water=0)
|
|
s.tick(10)
|
|
s.player(water=1)
|
|
s.tick(3)
|
|
s.player(water=0)
|
|
s.tick(2)
|
|
s.player(water=1)
|
|
s.tick(60)
|
|
s.player(water=1, spectator=1)
|
|
s.tick(5)
|
|
s.player(water=0, spectator=1)
|
|
s.tick(5)
|
|
s.player(water=1, removed=1)
|
|
s.tick(5)
|
|
s.player(water=0)
|
|
s.tick(25)
|
|
# Silent ambient category: loops never start and linger.
|
|
s.add('start', 4)
|
|
s.add('volume', 8, h.bits(0.0))
|
|
s.sounds(by_loop[0])
|
|
s.tick(50)
|
|
s.add('volume', 8, h.bits(1.0))
|
|
s.sounds(by_loop[1])
|
|
s.tick(50)
|
|
s.sounds(by_loop[0])
|
|
s.tick(50)
|
|
# Bubble columns: entering, staying, the first tick, spectators, small boxes.
|
|
s.add('start', 5)
|
|
column = [(0, 64, 0, 1), (0, 65, 0, 1)]
|
|
s.player((0.5, 64.0, 0.5), water=1, columns=column)
|
|
s.tick(3)
|
|
s.player((3.5, 64.0, 0.5), water=1, columns=column)
|
|
s.tick(2)
|
|
s.player((0.5, 64.0, 0.5), water=1, columns=column)
|
|
s.tick(2)
|
|
s.player((0.5, 64.0, 0.5), water=1, columns=[(0, 64, 0, 0), (1, 64, 0, 1)])
|
|
s.tick(2)
|
|
s.player((3.5, 64.0, 0.5))
|
|
s.tick(2)
|
|
s.player((0.9, 64.5, 0.5), water=1, height=0.6, columns=[(1, 64, 0, 0), (0, 64, 0, 1)])
|
|
s.tick(2)
|
|
s.player((3.5, 64.0, 0.5))
|
|
s.tick(1)
|
|
s.player((0.5, 63.7, 0.5), spectator=1, columns=column)
|
|
s.tick(2)
|
|
|
|
|
|
def fuzz(s, rng, values, steps):
|
|
s.add('start', rng.getrandbits(63))
|
|
for _ in range(steps):
|
|
op = rng.random()
|
|
if op < 0.05:
|
|
v = rng.choice(values + [dict(loop=None, mood=None, additions=[])])
|
|
if rng.random() < 0.5 and v['mood']:
|
|
v = dict(v, mood=(v['mood'][0], rng.choice([20, 60, 150]), rng.choice([1, 4, 8, 16]), rng.choice([0.0, 2.0, 5.5])))
|
|
s.sounds(v)
|
|
elif op < 0.12:
|
|
at = (rng.uniform(-3e4, 3e4) if rng.random() < 0.2 else rng.uniform(-200, 200), rng.uniform(-64, 320), rng.uniform(-200, 200))
|
|
columns = []
|
|
if rng.random() < 0.3:
|
|
bx, by, bz = int(at[0] // 1), int(at[1] // 1), int(at[2] // 1)
|
|
columns = [(bx + rng.randrange(-1, 2), by + rng.randrange(-1, 3), bz + rng.randrange(-1, 2), rng.randrange(2)) for _ in range(rng.randrange(1, 4))]
|
|
s.player(at, rng.choice([1.62, 1.27, 0.4, 0.2]), int(rng.random() < 0.4), int(rng.random() < 0.05), int(rng.random() < 0.03), rng.randrange(4),
|
|
rng.choice([1.8, 1.5, 0.6]), columns)
|
|
elif op < 0.13:
|
|
s.add('volume', rng.choice([0, 8]), h.bits(rng.choice([0.0, 1.0, 0.5])))
|
|
else:
|
|
s.tick(rng.randrange(1, 40))
|
|
|
|
|
|
def fixtures():
|
|
values = settings()
|
|
s = Script()
|
|
scripted(s, values)
|
|
rng = random.Random(0x414d4249)
|
|
for _ in range(40):
|
|
fuzz(s, rng, values, 150)
|
|
return s.lines
|
|
|
|
|
|
def main():
|
|
lines = fixtures()
|
|
matched, report = h.run_harness(
|
|
'ambient', 'VanillaAmbientOracle', 'ambient_fixture', ''.join(l + '\n' for l in lines),
|
|
extra_sources=[h.lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaSoundRig.java',
|
|
h.lab.ROOT / 'crates/horizon-client/src/ambience.rs',
|
|
h.lab.ROOT / 'crates/horizon-client/examples/probe/audio_common/mod.rs'],
|
|
java_args=[h.RESOURCES], rust_args=[h.RESOURCES],
|
|
scope='Real LocalPlayer.updateIsUnderwater, UnderwaterAmbientSoundHandler and BiomeAmbientSoundsHandler (loop cross-fades, additions, '
|
|
'cave mood sampling, moodiness and the mood sound position) with UnderLiquidAmbientSoundInstance/UnderLiquidSubSound/LoopSoundInstance '
|
|
'ticking in the real SoundEngine: every level random draw, handler state, playing instance and notification per tick. The '
|
|
'AMBIENT_SOUNDS value, eyes-in-water state and brightness are inputs. BubbleColumnAmbientSoundHandler runs on a scripted block world.')
|
|
print(f'{matched:,} ambient lines match the pinned client ({len(lines):,} steps): {report}', flush=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|