mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
287 lines
13 KiB
Python
287 lines
13 KiB
Python
"""Many-entity stress scenes for tools/perf_bench.py (`--scenes stress` or by name).
|
|
|
|
Each scene is built by vanilla commands, the same lines for every game (Horizon through horizon-ctl `command`,
|
|
vanilla and vanilla-mods through the bench driver mod), on a stone floor at y 289 high above the world spawn of
|
|
seed 1234 (loaded already: no terrain to generate), then held still for the flight's seconds, looking along +Z at
|
|
the scene. Its cleanup commands then remove every entity and block it made, before the next scene.
|
|
|
|
Vanilla renders an entity only within 64 * viewScale * its box size (Entity.shouldRenderAtSqrDistance), with
|
|
viewScale = clamp(render distance / 8, 1, 2.5): a dropped item (0.25) only within 32 blocks at RD 16. The scenes
|
|
keep everything within ~30 blocks of the camera and inside its view, so run them at RD 16 or more.
|
|
|
|
Entities are still and never despawn (NoAI, NoGravity, PersistenceRequired; items lie on barriers): the same picture every
|
|
frame and every run. Particles come from repeating command blocks under the floor, so they keep coming at the
|
|
same rate. Chests stay closed: opening one needs a player, which neither driver has.
|
|
"""
|
|
|
|
FLOOR_Y = 289
|
|
# The player's feet (the eyes are 1.62 higher).
|
|
PLAYER_Y = 298.0
|
|
PITCH = 15.0
|
|
# The camera stands at the origin, above the spawn of seed 1234 (-519.5, 58.5); the scene lies in front of it, at +Z.
|
|
ORIGIN = (-520, 40)
|
|
# What a scene may touch: x and z around the origin, y from the command blocks to above the TNT.
|
|
BOX = (-24, 24, 2, 40, FLOOR_Y - 9, FLOOR_Y + 26)
|
|
|
|
SETTINGS = [
|
|
# The setup's own commands and the command blocks would fill the chat.
|
|
"gamerule send_command_feedback false",
|
|
"gamerule command_block_output false",
|
|
"gamerule advance_time false",
|
|
"gamerule spawn_mobs false",
|
|
"time set 6000",
|
|
]
|
|
MOBS = ["husk", "creeper", "cow", "pig", "sheep", "chicken", "villager", "spider"]
|
|
HOLDERS = ["husk", "wither_skeleton", "vindicator", "piglin"]
|
|
# Mobs with an eyes render type layer that do not burn at noon.
|
|
EYED = ["spider", "cave_spider", "enderman"]
|
|
HELD = ["iron_sword", "stone_axe", "torch", "apple", "bow", "carrot", "diamond_pickaxe", "stone"]
|
|
ITEMS = ["stone", "oak_log", "diamond", "apple", "iron_sword", "bread", "redstone", "torch"]
|
|
FRAMED = ["diamond_sword", "compass", "clock", "stone", "oak_planks", "apple", "shield", "golden_apple"]
|
|
CONTAINERS = ["chest", "trapped_chest", "ender_chest", "white_shulker_box"]
|
|
BANNER = ('{patterns:[{pattern:"minecraft:stripe_top",color:"red"},{pattern:"minecraft:creeper",color:"black"},'
|
|
'{pattern:"minecraft:border",color:"blue"}]}')
|
|
SIGN_TEXT = '{front_text:{messages:["Horizon","stress","sign","wall"]}}'
|
|
|
|
|
|
def num(v):
|
|
"""A coordinate as commands write it: no trailing zeros, never an exponent."""
|
|
text = f"{v:.3f}".rstrip("0").rstrip(".")
|
|
return "0" if text == "-0" else text
|
|
|
|
|
|
def floor(ox, oz):
|
|
return [f"fill {ox - 24} {FLOOR_Y} {oz + 2} {ox + 24} {FLOOR_Y} {oz + 40} smooth_stone"]
|
|
|
|
|
|
def wall(ox, oz):
|
|
"""A stone wall facing the camera, 17 blocks ahead: the backing of frames and wall signs."""
|
|
return [f"fill {ox - 16} {FLOOR_Y - 1} {oz + 17} {ox + 17} {FLOOR_Y + 16} {oz + 17} stone"]
|
|
|
|
|
|
def empty(ox, oz):
|
|
return []
|
|
|
|
|
|
def mobs(ox, oz):
|
|
"""1000 mobs of 8 common kinds: 20 x 10 x 5 layers, 1.5 / 2 / 2.5 blocks apart."""
|
|
out, i = [], 0
|
|
for layer in range(5):
|
|
for row in range(10):
|
|
for col in range(20):
|
|
kind = MOBS[i % len(MOBS)]
|
|
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
|
|
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
|
|
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b}")
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def markings(ox, oz):
|
|
"""1000 horses with markings and slimes (translucent layers): the mobs scene's grid."""
|
|
out, i = [], 0
|
|
for layer in range(5):
|
|
for row in range(10):
|
|
for col in range(20):
|
|
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
|
|
# Horse.Variant: the colour, plus 256 times the markings (1 to 4).
|
|
kind = (f"horse {{Variant:{i % 7 + 256 * (i // 2 % 4 + 1)}," if i % 2 == 0
|
|
else "slime {Size:1,")
|
|
name, tags = kind.split(" ")
|
|
out.append(f"summon {name} {num(x)} {num(y)} {num(z)} "
|
|
f"{tags}NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b}}")
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def glowing(ox, oz):
|
|
"""1000 glowing mobs (the outline post effect): the mobs scene's kinds and grid."""
|
|
out, i = [], 0
|
|
for layer in range(5):
|
|
for row in range(10):
|
|
for col in range(20):
|
|
kind = MOBS[i % len(MOBS)]
|
|
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
|
|
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
|
|
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b,Glowing:1b}")
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def eyes(ox, oz):
|
|
"""1000 mobs with glowing eyes (spiders, cave spiders, endermen): the mobs scene's grid."""
|
|
out, i = [], 0
|
|
for layer in range(5):
|
|
for row in range(10):
|
|
for col in range(20):
|
|
kind = EYED[i % len(EYED)]
|
|
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
|
|
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
|
|
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b}")
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def held(ox, oz):
|
|
"""1000 mobs holding an item in each hand: the mobs scene's grid, of kinds that neither burn nor convert."""
|
|
out, i = [], 0
|
|
for layer in range(5):
|
|
for row in range(10):
|
|
for col in range(20):
|
|
kind = HOLDERS[i % len(HOLDERS)]
|
|
main, off = HELD[i % len(HELD)], HELD[(i + 3) % len(HELD)]
|
|
x, y, z = ox + 0.5 - 14.25 + 1.5 * col, FLOOR_Y + 1 + 2.5 * layer, oz + 8 + 2 * row
|
|
out.append(f"summon {kind} {num(x)} {num(y)} {num(z)} "
|
|
"{NoAI:1b,NoGravity:1b,PersistenceRequired:1b,Silent:1b,IsImmuneToZombification:1b,"
|
|
f'equipment:{{mainhand:{{id:"minecraft:{main}"}},offhand:{{id:"minecraft:{off}"}}}}}}')
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def items(ox, oz):
|
|
"""2000 dropped items of 8 kinds: 25 x 16 on the floor and on four invisible barrier layers above it, 2 blocks
|
|
apart, a block apart in a layer (vanilla merges equal stacks within 0.625). They fall the last 0.2 block onto
|
|
their layer: no NoGravity, which not every game reads for items."""
|
|
out = [f"fill {ox - 12} {FLOOR_Y + 2 * layer} {oz + 8} {ox + 12} {FLOOR_Y + 2 * layer} {oz + 23} barrier"
|
|
for layer in range(1, 5)]
|
|
i = 0
|
|
for layer in range(5):
|
|
for row in range(16):
|
|
for col in range(25):
|
|
item = ITEMS[i % len(ITEMS)]
|
|
x, y, z = ox + 0.5 - 12 + col, FLOOR_Y + 1.2 + 2 * layer, oz + 8.5 + row
|
|
out.append(f"summon item {num(x)} {num(y)} {num(z)} "
|
|
f'{{Item:{{id:"minecraft:{item}",count:1}},Age:-32768s,PickupDelay:32767s}}')
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def frames(ox, oz):
|
|
"""512 item frames holding 8 kinds of item on a wall (32 x 16)."""
|
|
out = wall(ox, oz)
|
|
i = 0
|
|
for y in range(FLOOR_Y, FLOOR_Y + 16):
|
|
for x in range(ox - 15, ox + 17):
|
|
item = FRAMED[i % len(FRAMED)]
|
|
# block_pos too: summoning places the entity, not the block a frame hangs in.
|
|
out.append(f"summon item_frame {x} {y} {oz + 16} {{block_pos:[I;{x},{y},{oz + 16}],"
|
|
f'Facing:2b,Fixed:1b,Item:{{id:"minecraft:{item}",count:1}}}}')
|
|
i += 1
|
|
return out
|
|
|
|
|
|
def armor_stands(ox, oz):
|
|
"""500 armor stands in full armour holding a sword (25 x 20)."""
|
|
gear = ('{ShowArms:1b,NoGravity:1b,equipment:{head:{id:"minecraft:diamond_helmet"},'
|
|
'chest:{id:"minecraft:iron_chestplate"},legs:{id:"minecraft:golden_leggings"},'
|
|
'feet:{id:"minecraft:leather_boots"},mainhand:{id:"minecraft:diamond_sword"}}}')
|
|
return [f"summon armor_stand {num(ox + 0.5 - 12 + col)} {FLOOR_Y + 1} {oz + 8 + row} {gear}"
|
|
for row in range(20) for col in range(25)]
|
|
|
|
|
|
def avatars(ox, oz):
|
|
"""300 mannequins, the player model (20 x 15, 1.5 blocks apart): what cosmetics cost on many players
|
|
(docs/cosmetics.md: `HORIZON_COSMETICS=1 HORIZON_COSMETICS_LOCAL=<items>` dresses every mannequin)."""
|
|
return [f"summon mannequin {num(ox + 0.5 - 14.25 + 1.5 * col)} {FLOOR_Y + 1} {num(oz + 6 + 1.5 * row)} "
|
|
"{NoGravity:1b,Silent:1b,Rotation:[180f,0f]}"
|
|
for row in range(15) for col in range(20)]
|
|
|
|
|
|
def chests(ox, oz):
|
|
"""800 containers with block-entity models: four walls 25 wide and 8 high, one kind each."""
|
|
return [f"fill {ox - 12} {FLOOR_Y + 1} {oz + 10 + 4 * i} {ox + 12} {FLOOR_Y + 8} {oz + 10 + 4 * i} {kind}"
|
|
for i, kind in enumerate(CONTAINERS)]
|
|
|
|
|
|
def signs(ox, oz):
|
|
"""A wall of 512 written wall signs (32 x 16) and a field of 175 standing ones in front of it."""
|
|
return wall(ox, oz) + [
|
|
f"fill {ox - 15} {FLOOR_Y} {oz + 16} {ox + 16} {FLOOR_Y + 15} {oz + 16} oak_wall_sign[facing=north]{SIGN_TEXT}",
|
|
f"fill {ox - 12} {FLOOR_Y + 1} {oz + 6} {ox + 12} {FLOOR_Y + 1} {oz + 12} oak_sign[rotation=8]{SIGN_TEXT}",
|
|
]
|
|
|
|
|
|
def banners(ox, oz):
|
|
"""300 waving banners with three patterns: 12 rows of 25."""
|
|
return [f"fill {ox - 12} {FLOOR_Y + 1} {oz + 8 + 2 * row} {ox + 12} {FLOOR_Y + 1} {oz + 8 + 2 * row} "
|
|
f"white_banner[rotation=8]{BANNER}" for row in range(12)]
|
|
|
|
|
|
def particles(ox, oz):
|
|
"""Steady heavy particles: repeating command blocks under the floor, powered by a redstone block under each,
|
|
every tick: flames, smoke, end rods, firework rockets that burst over the floor, and TNT that explodes in the
|
|
air (nothing within its reach)."""
|
|
def block(dx, dz, command):
|
|
command = command.replace('"', '\\"')
|
|
at = f"{ox + dx} {FLOOR_Y - 8} {oz + dz}"
|
|
return [f'setblock {at} repeating_command_block{{Command:"{command}"}}',
|
|
f"setblock {ox + dx} {FLOOR_Y - 9} {oz + dz} redstone_block"]
|
|
|
|
x, z = ox + 0.5, oz + 20.5
|
|
emitters = [
|
|
("flame", 80, 0.01), ("flame", 80, 0.01), ("flame", 80, 0.01), ("flame", 80, 0.01),
|
|
("campfire_cosy_smoke", 4, 0.0), ("campfire_cosy_smoke", 4, 0.0),
|
|
("end_rod", 40, 0.05), ("end_rod", 40, 0.05),
|
|
]
|
|
out = [line for i, (kind, count, speed) in enumerate(emitters) for line in
|
|
block(-8 + 2 * i, 4, f"particle minecraft:{kind} {num(x)} {FLOOR_Y + 5} {num(z)} 8 4 8 {speed} {count} force")]
|
|
rocket = ('{LifeTime:18,FireworksItem:{id:"minecraft:firework_rocket",count:1,components:{"minecraft:fireworks":'
|
|
'{explosions:[{shape:"large_ball",colors:[I;16711680,65280,255],has_trail:1b}]}}}}')
|
|
out += [line for i in range(3) for line in
|
|
block(-8 + 2 * i, 6, f"summon firework_rocket {num(x - 6 + 6 * i)} {FLOOR_Y + 2} {num(z)} {rocket}")]
|
|
out += block(0, 8, f"summon tnt {num(x)} {FLOOR_Y + 24} {num(z + 6)} {{fuse:0}}")
|
|
return out
|
|
|
|
|
|
# name, builder: in the order --scenes stress runs them.
|
|
STRESS = [
|
|
("stress_empty", empty),
|
|
("stress_mobs", mobs),
|
|
("stress_items", items),
|
|
("stress_frames", frames),
|
|
("stress_armor_stands", armor_stands),
|
|
("stress_chests", chests),
|
|
("stress_signs", signs),
|
|
("stress_banners", banners),
|
|
("stress_particles", particles),
|
|
("stress_held", held),
|
|
("stress_eyes", eyes),
|
|
("stress_glowing", glowing),
|
|
("stress_avatars", avatars),
|
|
("stress_markings", markings),
|
|
]
|
|
NAMES = [name for name, _ in STRESS]
|
|
|
|
|
|
def origin(name):
|
|
return ORIGIN
|
|
|
|
|
|
def scene(name):
|
|
"""(name, from, to, pitch) as perf_bench's scenes: a hold at the camera, facing +Z."""
|
|
ox, oz = origin(name)
|
|
at = (ox + 0.5, PLAYER_Y, oz + 0.5)
|
|
return name, at, (at[0], at[1], at[2] + 0.01), PITCH
|
|
|
|
|
|
def setup(name, first=True):
|
|
"""The commands that build the scene, after its chunks are loaded; the game rules only for the first scene of
|
|
a run (setting a rule again says so in red in the chat, which would stay on screen)."""
|
|
ox, oz = origin(name)
|
|
return (SETTINGS if first else []) + floor(ox, oz) + dict(STRESS)[name](ox, oz)
|
|
|
|
|
|
def cleanup(name):
|
|
"""Removes the scene: its entities first (a frame would drop its item when its wall goes), then every block
|
|
in its box, in slabs under vanilla's fill limit, then anything that still dropped. Nothing it runs fails, so
|
|
no red line stays in the chat during the next scene: `execute as` kills nothing quietly, and a marker block in
|
|
each slab gives every fill something to clear."""
|
|
ox, oz = origin(name)
|
|
x0, x1, z0, z1, y0, y1 = BOX
|
|
kill = "execute as @e[type=!minecraft:player] run kill @s"
|
|
out = [kill]
|
|
for y in range(y0, y1 + 1, 12):
|
|
out.append(f"setblock {ox + x0} {y} {oz + z0} barrier")
|
|
out.append(f"fill {ox + x0} {y} {oz + z0} {ox + x1} {min(y + 11, y1)} {oz + z1} air")
|
|
return out + [kill]
|