Files
RustNotMC/tools/hud_parity_cases.py
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

259 lines
14 KiB
Python

"""Random HUD cases for test_hud_parity.py, one generator per case kind."""
EFFECTS = ['minecraft:poison', 'minecraft:wither', 'minecraft:regeneration', 'minecraft:hunger', 'minecraft:speed']
def decorations(rng, n):
max_health = rng.choice([20.0, 20.0, 6.0, 40.0, 33.0, 100.0])
health = rng.choice([max_health, round(rng.uniform(0, max_health), 2), 1.0, 0.5, 3.0])
air_max = 300
under = rng.random() < 0.3
air = rng.choice([air_max, rng.randint(-20, air_max), 0, 1, 29, 30, 31, 299]) if under or rng.random() < 0.2 else air_max
case = {
'width': rng.choice([427, 320, 480, 640, 961]), 'height': rng.choice([240, 255, 270, 360, 501]),
'gameMode': rng.choice(['survival'] * 5 + ['adventure', 'creative', 'spectator']),
'crosshair': rng.random() < 0.7,
'attackIndicator': rng.choice(['OFF', 'CROSSHAIR', 'HOTBAR']),
'attackScale': rng.choice([1.0, round(rng.random(), 3), 0.0, 0.999]),
# Minecraft.crosshairPickEntity (a living entity, alive or dead) and
# getCurrentItemAttackStrengthDelay, for the full indicator.
'target': rng.choice([None, True, True, False]),
'attackDelay': rng.choice([5.0, 12.5, 4.0, 20.0, 5.0001, 10.0]),
'selected': rng.randint(0, 8), 'rightHanded': rng.random() < 0.7,
'slots': [rng.choice([None, 0, 0, 3, 5]) if rng.random() < 0.6 else None for _ in range(9)],
'offhand': rng.choice([None, None, 0, 4]), 'partial': round(rng.random(), 3),
'health': health, 'maxHealth': max_health,
'absorption': rng.choice([0.0, 0.0, 4.0, 3.5, 17.0, 40.0]),
'armor': rng.choice([0, 0, 1, 7, 20, 13]), 'food': rng.randint(0, 20),
'saturation': rng.choice([0.0, 5.0, 0.0]),
'effects': [e for e in EFFECTS if rng.random() < 0.15], 'frozen': rng.random() < 0.08,
'hardcore': rng.random() < 0.2, 'damageCooldown': rng.choice([0, 0, 10]),
'air': air, 'maxAir': air_max, 'underWater': under,
'tick': rng.randint(0, 100000), 'lastHealth': rng.randint(0, 40), 'displayHealth': rng.randint(0, 40),
'lastHealthAgo': rng.choice([0, 500, 5000]), 'healthBlinkTime': 0,
'xpProgress': rng.choice([0.0, round(rng.random(), 4), 0.999]), 'xpLevel': rng.choice([0, 1, 7, 30, 123, 4000]),
'experienceBar': rng.random() < 0.8,
}
if case['target'] and rng.random() < 0.5:
case['attackScale'] = 1.0
if case['damageCooldown'] and rng.random() < 0.5:
case['healthBlinkTime'] = case['tick'] + rng.randint(1, 20)
if rng.random() < 0.15:
case['vehicleMaxHealth'] = rng.choice([15.0, 30.0, 53.0, 100.0])
case['vehicleHealth'] = round(rng.uniform(0, case['vehicleMaxHealth']), 1)
return case
ALL_EFFECTS = ['speed', 'slowness', 'haste', 'mining_fatigue', 'strength', 'instant_health', 'instant_damage', 'jump_boost',
'nausea', 'regeneration', 'resistance', 'fire_resistance', 'water_breathing', 'invisibility', 'blindness',
'night_vision', 'hunger', 'weakness', 'poison', 'wither', 'health_boost', 'absorption', 'saturation', 'glowing',
'levitation', 'luck', 'unluck', 'slow_falling', 'conduit_power', 'dolphins_grace', 'bad_omen',
'hero_of_the_village', 'darkness', 'trial_omen', 'raid_omen', 'wind_charged', 'weaving', 'oozing', 'infested',
'breath_of_the_nautilus']
SIZES = dict(width=[427, 320, 480, 640, 961], height=[240, 255, 270, 360, 501])
def screen(rng):
return {'width': rng.choice(SIZES['width']), 'height': rng.choice(SIZES['height'])}
def backdrop(rng):
"""The text background options behind textWithBackdrop."""
return {'chatOnly': rng.random() < 0.5, 'textOpacity': rng.choice([0.0, 0.5, 0.25, 1.0, round(rng.random(), 3)])}
def effects(rng, n):
count = rng.choice([1, 2, 3, 5, 8, 12, 20])
active = []
for e in rng.sample(ALL_EFFECTS, count):
duration = rng.choice([-1, 1, 7, 19, 20, 99, 150, 199, 200, 201, 600, 32147, 32148, 40000, rng.randint(0, 220)])
active.append({'id': 'minecraft:' + e, 'duration': duration, 'ambient': rng.random() < 0.25, 'icon': rng.random() < 0.9})
return {**screen(rng), 'demo': rng.random() < 0.15, 'active': active}
TITLES = [{'text': 'Hello'}, {'text': 'Boss fight!', 'color': 'red', 'bold': True}, {'text': 'Wave 3', 'italic': True},
{'text': 'A long title over the whole screen'}, {'text': ''}, ['Mixed ', {'text': 'parts', 'color': 'gold'}],
{'text': 'Ünïcödé ★'},
# Servers send legacy formatting codes inside a component's text; the font applies them:
{'text': '§8[§fAny questions? §8] §e§l/support en'}, {'text': '§cRed §rreset §', 'color': 'blue'}]
def title(rng, n):
"""Title, subtitle and action bar events between ticks (fades, setTimes mid-title, clears)."""
ops = []
for _ in range(rng.randint(1, 6)):
r = rng.random()
if r < 0.3:
ops.append(['title', rng.choice(TITLES)])
elif r < 0.45:
ops.append(['subtitle', rng.choice(TITLES)])
elif r < 0.6:
ops.append(['times', rng.choice([-1, 0, 1, 5, 10, 20]), rng.choice([-1, 0, 3, 40, 70]), rng.choice([-1, 0, 1, 7, 20])])
elif r < 0.65:
ops.append(['clear'])
elif r < 0.7:
ops.append(['reset'])
elif r < 0.85:
ops.append(['overlay', rng.choice(TITLES), rng.random() < 0.3])
ops.append(['tick', rng.choice([0, 0, 1, 2, 5, 9, 10, 11, 30, 50, 59, 60, 79, 80, 81, 99, 100, rng.randint(0, 120)])])
return {**screen(rng), **backdrop(rng), 'partial': round(rng.random(), 3), 'ops': ops}
# Items for the selected item name: id, translation key and rarity colour (checked by the oracle's own stacks).
HELD = [('stone', 'block.minecraft.stone', 'white'), ('diamond_sword', 'item.minecraft.diamond_sword', 'white'),
('totem_of_undying', 'item.minecraft.totem_of_undying', 'yellow'), ('beacon', 'block.minecraft.beacon', 'aqua'),
('dragon_egg', 'block.minecraft.dragon_egg', 'light_purple'), ('apple', 'item.minecraft.apple', 'white')]
CUSTOM = [None, None, None, {'text': 'Excalibur'}, {'text': 'Red', 'color': 'red'}, {'text': 'Plain', 'italic': False}]
def selected(rng, n):
"""Selected-slot changes tick by tick, then the name with its fade."""
def stack():
item, key, rarity = rng.choice(HELD)
return {'item': item, 'key': key, 'rarity': rarity, 'custom': rng.choice(CUSTOM)}
steps, held = [], stack()
for _ in range(rng.randint(1, 60)):
if rng.random() < 0.1:
if rng.random() < 0.2:
held = None
else:
held = stack()
steps.append(held)
return {**screen(rng), **backdrop(rng), 'gameMode': rng.choice(['survival', 'adventure', 'creative']),
'displayTime': rng.choice([1.0, 1.0, 0.5, 2.0, 0.123]), 'steps': steps}
def boss(rng, n):
bars = []
for _ in range(rng.choice([1, 1, 2, 3, 5, 9])):
progress = rng.choice([0.0, 1.0, 0.5, 0.001, 0.999, 1 / 182, round(rng.random(), 4)])
bars.append({'name': rng.choice(TITLES), 'progress': progress, 'color': rng.randrange(7), 'overlay': rng.randrange(5)})
return {**screen(rng), 'bars': bars}
def jump(rng, n):
return {**screen(rng), 'cooldown': rng.choice([0, 0, 0, 1, 10]),
'jumpScale': rng.choice([0.0, 1.0, 0.4, 0.001, round(rng.random(), 4)])}
# Servers' sidebars use legacy formatting codes in fake owners, team affixes and titles (#937):
# a code, an unknown code, `§r` and a trailing lone `§`.
OWNERS = ['Alex', 'steve', 'Zed', 'bob', 'Bobby', 'ÉLodie', 'élan', '#hidden', 'x', 'LongPlayerName_123', 'Mia', '_under',
'§1§r', '§a§lPro§r name', 'end§', '§zodd§Rcase']
FORMATS = [None, None, None, {'type': 'blank'}, {'type': 'fixed', 'value': {'text': 'FIX', 'color': 'aqua'}},
{'type': 'styled', 'style': {'color': 'gold', 'bold': True}}, {'type': 'styled', 'style': {}}]
def sidebar(rng, n):
"""An objective with scores, display names, number formats and teams."""
owners = rng.sample(OWNERS, rng.randint(0, len(OWNERS)))
if rng.random() < 0.3: # more rows than the 15 shown
owners += [f'p{i}' for i in range(rng.randint(5, 20))]
scores = [{'owner': o, 'value': rng.choice([0, 1, -1, 7, 7, 100, 2147483647, -2147483648, rng.randint(-50, 50)]),
'display': rng.choice([None, None, None, {'text': 'Shown ' + o, 'color': 'green'}]),
'format': rng.choice(FORMATS)} for o in owners]
teams = []
for t in range(rng.choice([0, 0, 1, 2])):
teams.append({'name': f'team{t}', 'prefix': rng.choice([{'text': ''}, {'text': '[A] '}, {'text': '<', 'color': 'red'},
{'text': '§c§lRed §r', 'color': 'blue'}]),
'suffix': rng.choice([{'text': ''}, {'text': ' *'}, {'text': ' §7(§e5§7)§'}]),
'color': rng.choice([None, 'red', 'blue', 'gold', 'white']),
'members': [o for o in owners if rng.random() < 0.3]})
return {**screen(rng), **backdrop(rng),
'objective': {'display': rng.choice(TITLES + [{'text': 'Kills'}, {'text': '§e§lANTICHEAT §7test'}]),
'format': rng.choice(FORMATS + [{'type': 'fixed', 'value': {'text': '§a12§'}}])},
'scores': scores, 'teams': teams}
# Items for the decorations: id and max damage (0 = not damageable), checked by the oracle's own stacks.
DECOR_ITEMS = [('diamond_sword', 1561), ('bow', 384), ('shield', 336), ('flint_and_steel', 64), ('elytra', 432),
('stone', 0), ('ender_pearl', 0), ('apple', 0)]
def decor(rng, n):
"""GuiGraphicsExtractor.itemDecorations: count, durability bar and cooldown shade."""
item, max_damage = rng.choice(DECOR_ITEMS)
case = {'x': rng.randint(-20, 400), 'y': rng.randint(-20, 300), 'item': item, 'maxDamage': max_damage,
'count': rng.choice([1, 1, 2, 16, 64, 99, 0, 10, 127]),
'damage': rng.choice([0, 0, 1, max_damage // 2, max_damage - 1, max_damage, rng.randint(0, max(max_damage, 1))]) if max_damage else 0,
'unbreakable': rng.random() < 0.1, 'partial': round(rng.random(), 3)}
if rng.random() < 0.4:
duration = rng.choice([1, 2, 7, 20, 100, 200])
case['cooldown'] = {'duration': duration, 'elapsed': rng.choice([0, 1, duration - 1, duration, rng.randint(0, duration)])}
if rng.random() < 0.1:
case['countText'] = rng.choice(['', '+', '1/2', 'x9'])
return case
def overlays(rng, n):
"""Camera overlays (vignette and border warning, spyglass, pumpkin, powder snow, portal, nausea) and sleep."""
case = {**screen(rng), 'vignette': rng.random() < 0.7,
'vignetteBrightness': rng.choice([1.0, 0.0, 0.5, round(rng.random(), 4), 1.3, -0.2]),
'firstPerson': rng.random() < 0.8, 'scoping': rng.random() < 0.2,
'scopeScale': rng.choice([0.5, 1.125, round(rng.uniform(0.5, 1.125), 4)]), 'partial': round(rng.random(), 3),
'head': rng.choice([None, None, 'carved_pumpkin', 'diamond_helmet']),
'ticksFrozen': rng.choice([0, 0, 0, 1, 70, 139, 140, 300]),
'portal': rng.choice([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0125], [0.5, 0.5125], [1.0, 1.0], [0.05, 0.0], [round(rng.random(), 3)] * 2]),
'nausea': rng.choice([0.0, 0.0, 1.0, 0.25, round(rng.random(), 3)]),
'screenEffectScale': rng.choice([1.0, 0.0, 0.5, round(rng.random(), 3)]),
'sleepTimer': rng.choice([0, 0, 0, 1, 50, 100, 101, 105, 110, 111])}
if rng.random() < 0.4:
size = rng.choice([10.0, 50.0, 200.0, 1000.0])
case['border'] = {'centerX': rng.choice([0.0, 100.5]), 'centerZ': rng.choice([0.0, -30.25]), 'size': size,
'warningBlocks': rng.choice([0, 5, 20, 100]), 'warningTime': rng.choice([0, 15])}
case['camX'] = round(rng.uniform(-150, 250), 2)
case['camZ'] = round(rng.uniform(-150, 150), 2)
return case
WORDS = ['hello', 'world', 'a', 'Minecraft', 'supercalifragilisticexpialidocious', 'x' * 70, 'ok!', 'Ünïcödé', '★', '']
def chat_text(rng):
words = [rng.choice(WORDS) for _ in range(rng.choice([1, 2, 5, 12, 30]))]
text = rng.choice([' ', ' ', ' ', ' ', '\n']).join(words)
if rng.random() < 0.1:
text = rng.choice([' ', '\n']) + text + rng.choice(['', ' ', '\n'])
if rng.random() < 0.3:
cut = rng.randint(0, len(text))
return [{'text': text[:cut], 'color': rng.choice(['red', 'gold', 'aqua'])},
{'text': text[cut:], 'bold': rng.random() < 0.5, 'italic': rng.random() < 0.3}]
return {'text': text}
def chat(rng, n):
"""Messages wrapped to the chat width and fading 200 ticks after they arrive."""
count = rng.choice([1, 2, 5, 10, 30, 120])
now = rng.randint(200, 2000)
messages = []
for _ in range(count):
at = rng.choice([now, now - 1, now - 179, now - 180, now - 190, now - 199, now - 200, now - 500, rng.randint(now - 260, now + 5)])
messages.append({'text': chat_text(rng), 'at': at, 'tag': rng.choice([None, None, 'system', 'error'])})
messages.sort(key=lambda m: m['at'])
return {**screen(rng), 'now': now, 'messages': messages,
'chatScale': rng.choice([1.0, 1.0, 0.5, 0.0 + 0.75, 0.25]), 'chatWidth': rng.choice([1.0, 1.0, 0.5, 0.0, 0.3]),
'chatOpacity': rng.choice([1.0, 0.5, 0.0, 0.25]), 'textOpacity': rng.choice([0.5, 0.0, 1.0, 0.25]),
'chatLineSpacing': rng.choice([0.0, 0.0, 0.5, 1.0, 0.25]), 'chatHeightUnfocused': rng.choice([70 / 160, 1.0, 0.0, 0.5]),
'chatHeightFocused': rng.choice([1.0, 1.0, 0.5, 0.25]),
# The chat screen open: foreground, focused height, scrolled up or down.
'focused': rng.random() < 0.5, 'scroll': rng.choice([0, 0, 1, 3, 7, 20, -2, 1000])}
# Generators by kind: (rng, index) -> case. Cases are drawn round-robin.
KINDS = {
'decorations': decorations,
'effects': effects,
'title': title,
'selected': selected,
'boss': boss,
'jump': jump,
'sidebar': sidebar,
'decor': decor,
'overlays': overlays,
'chat': chat,
}