Rebased onto main (the writer loop now lives in save/writes.rs): batches under
4 chunks stay on the caller, coalescing is capped at 64 chunks, the pool shuts
down on drop and a worker panic no longer takes the region writer with it.
Co-Authored-By: Mik00000 <[email protected]>
Co-Authored-By: darkliro <[email protected]>
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
With HORIZON_PARALLEL_TICK on, the spawn attempts of the candidate chunks run on
the tick's helper threads against the caps as the pass started, each chunk on its
own random; the server thread then commits the proposals in the shuffled chunk
order, rechecking the player cap, the spawn charge and the overlap with mobs
accepted so far (#16132).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
GameLevel becomes Level<H> (H: the &Game of today's pass); what it read of
Game besides its MobWorld is in SpawnHost; biome facts, structure lookups and
the finalized mob come from functions over world data. No behaviour change:
groundwork for running the spawn attempts on helper threads (#16132).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A mob that draws from the level's one random waits until every other lane has
passed its place in the list (OrderedDraws). A fish leading a school draws on
every tick, a parrot too, and brains draw their durations, so on a busy level
the lanes took turns instead of running side by side.
While the parallel tick is on, a turn's level-random draws now come from a
random seeded from the mob's id and the game time, as the villagers' brains
already did. Vanilla's odds, not its values. Nothing changes with it off.
Fixes#16125
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Keep every API entry, separating entries with HTML breaks so generated history does not grow the oversized hook module. No hook implementations change.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
The `while let` scrutinee kept the queue's MutexGuard alive through the callback, so only one
chunk ran at a time. Pop the chunk in its own statement. A test makes one chunk wait (bounded)
for another to start, so a serial run fails instead of hanging, and checks each item runs once.
Also: server_bench's populate docstring no longer says placements are the same every run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- A hold ended without its arrival (a second /tp to lit land, a trip to
another level, a respawn, a death) left the guest's `landing` set, so the
server ignored its moves until a relog. `release_held_arrivals` now clears it.
- A mod that cancels or redirects the teleport (`native_player_teleport`)
moves the hold with the player instead of leaving it on the old target.
- A hold's arrival only re-sends the position: no portal cooldown, no new body.
- `native_terrain_surface` names biomes through the world's own table
(data pack biomes), and the docs say what None, the Nether height and the
top block mean. `NativeTerrainColumn` is `non_exhaustive`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
SaveDir::saved_portal_blocks cached every None from load, including
record-read and upgrade errors, so a chunk that failed once hid its
portal until it was saved again. A read that failed is now retried:
only chunks confirmed absent or loaded are kept (try_load tells them
apart). Portal-less chunks share one empty Arc. The test also breaks
a record's compression ID, queries, restores it and queries again.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- Remove the TEMP-DIAG hooks from the mob islands plan/run paths, the shape bench that
read them, and horizon-core's `take_waited`, which only they used.
- A villager's own-random brain tick seeds from the mob id and game time instead of one
`next_long` of its entity random, so the entity random keeps vanilla's sequence.
- `OrderedDraws::wait_until`: a SeqCst fence after registering as a sleeper, so a wake is
not missed on weakly ordered CPUs.
- Note on `OWN_RANDOM_TEST` that it is process-wide.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The brain-off restock accepted any POI at the remembered job site (a bed head,
a bell, another profession's workstation) and ignored distance. It now needs a
Poi::Job of the villager's own profession (PoiFilter::Held) within 1.73 blocks
(WorkAtPoi.checkExtraStartConditions). Note on the server config default that
it must match lobotomy::DEFAULT_INTERVAL.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A mob that floats (in water or lava deep enough) keeps its full AI, so
FloatGoal and the brains' Swim still jump every tick instead of letting a
far mob sink, drown or burn.
With DAB on, serverAiStep's full/partial selector alternation counts the
mob's AI turns instead of its age: at an even interval every AI turn of
an odd-id mob fell on the partial phase, so it never started a new goal
or target.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The scan counts a cell's outer coordinates without an iterator chain, keeps
the moved full cube of each block it meets in a per-thread table, and
supporting_block asks a block's boxes without collecting them.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
- Event::Test/Spawn borrow the entity id, so the game's no-op trace
builds no String per attempt.
- SpawnState::last_checked drops its String (after_spawn always follows
can_spawn for the same type and position).
- weighted_pick returns a reference; the attempt keeps a Cow, cloning
only when a mod hook owns the list.
- SpawnState reuses its players_near map, near list, charges, counts and
scratch Vecs across ticks (SpawnBuffers); the per-chunk tracked Vec
and the per-start nearby-players Vec are reused too.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
decide looked up each merged pull request with its own `gh pr view`. About 2,000
merges piled up since the last Nightly (11-08), so every decision ran out of the
workflow token's GraphQL quota half way ("API rate limit already exceeded") and
no Nightly could go out. One GraphQL query now fetches 50 pull requests.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Two- and four-core isolated runs regress at 16/64 and 120/120, while the whole tracker improves in every paired round at 400/400 and 600/600. Raise both cutoffs to 400 and retain the measurements and limitations.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Past ForcedCover::MOST (4,096) forced chunks no distance map was made and
near_forced scanned every forced chunk for each column in_main_range asked
about, several times a tick: with ~5,800 chunks held, server ticks of
400-800 ms, nearly all in streaming.
The cover now also keeps the forced chunks as one u64 per 8x8 tile. Past
MOST (and for a reach past the map's REACH) a question reads only the tiles
its square overlaps, at most four for streaming's reaches: the same answer as
the scan, ~17-30 ns a question from 4,097 to 20,000 forced chunks against
4-23 us scanned, at most one key and word per forced chunk. Below MOST the
distance map answers as before. The unload sweep's scan of every forced chunk
for each far proto chunk asks the cover too (Ranges::forced_within).
Tests check the cover against the scan for discs, clusters and scattered sets
of 4,096 to 20,000 chunks and for squares cut by tile edges; forced_cover_cost
(ignored) prints the costs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob's first draw on the ordered level random made every lane wait for all earlier mobs, and in a farm-shaped
level all of those first draws were villager brains. With HORIZON_PARALLEL_TICK on, a villager's brain now draws
from a private random seeded from its own entity random (serial and grouped alike, so grouped still equals serial).
Ordered waits spin, yield, then sleep on a condvar. Adds pm_shaped_bench.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A tame wolf that teleported to Alice keeps strolling (WaterAvoidingRandomStrollGoal,
up to 10 blocks) and only walks back from 10 blocks, so a few seconds later it can
be 6 to 12 blocks from her, in vanilla as in Horizon. The 6-block check failed on
that at random (#15483; parity-main 43332620951 bisected it to unrelated merges).
Wolves left behind stay 28 blocks or more away.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
push_carts_from_mobs tested alive/species/reach on every mob of the level; a bitmap
of the carts' coarse cells now turns away a mob that cannot meet a cart after reading
only its position. The mob grid for cart pickups is built in one walk with the mounted
set. Adds an ignored timing test (cart_cost_bench).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Gate the benchmark allocator behind a dev-only feature so ordinary tests are unaffected, and reject benchmark binaries built without it.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Check the serial-all flag case-insensitively and keep the optimized-build guard usable under Clippy.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
Measure serial and parallel sending on identical threshold and crowd workloads, with packet and event checks outside the timed region. Require an explicit opt-in as well as --ignored so routine tests do not measure performance.
Refs #15858
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
[entities] dab in server.toml (off by default): a mob farther than
dab_start_distance from the nearest player runs its goals, brains and
customServerAiStep on one tick in clamp(d2 >> dab_activation_dist_mod, 1,
dab_max_tick_freq). Movement, controls, physics and despawn keep running.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Key the sampled totals by level and type id instead of the address of a
per-sample MobType clone, which split one type over many rows and pushed
others out of the 40 kept. start_mob keeps an Arc, not a copy. The kept rows
go from 40 to 512, enough for every type in every level.
Fixes#15847
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
[entities] max_collisions in server.toml (0 = off, vanilla) reaches the game as
HORIZON_MAX_ENTITY_COLLISIONS. A mob pushes at most N other mobs per tick, pushes it
receives counted; cramming still counts every entity in the box.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
VoxelShape::moved no longer clones the shape just to replace three of its
Arcs, resolve_world_movement sizes its shape list once, and push_entities
keeps its neighbour list per thread. Same shapes, same order, same results.
Refs #15848
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Slot ids 500-504 are the player's own armor and offhand, which its inventory always
lists; only 300-499 are a container's. The check failed on them at its first step,
after it had sent the guest to the Nether, and the checks that followed in the same run
then measured a guest in another level than the host.
Co-Authored-By: Claude <[email protected]>
horizon_threads::par_chunks_mut started helper OS threads under thread::scope on every
tracking pass. The sweep now uses parallel::par_chunks_mut, on the existing rayon
pool, shared out under the same conditions as before (not gated by parallel_tick;
HORIZON_SERIAL_TICK=all still keeps it serial). server_bench gains --entities to fill
the level with items, minecarts and chickens.
Refs #15796, #8158.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A trait mod pinned to Nightly 2026-11-08 (fdc87ae) failed to build: the
generated main always named horizon_threads::GameAllocator, which that
revision's horizon-threads doesn't have yet. Like names_mods, read the
staged source and declare GameAllocator/GAME_ALLOCATOR where it defines
them, else Allocator/ALLOCATOR.
Fixes#15764
An arrival in the Overworld searches 19 x 19 chunks for an exit portal and
decoded every one that isn't loaded from disk, for every entity that
arrives: the likely cause of the 19-27 ms late_portal_arrivals spikes of a
79-player server (#15798). The level's SaveDir now keeps the portal blocks
of each saved chunk it read until it queues that chunk to be written again.
Spans inside the arrival path (resolve_arrivals, exit_portal,
spawn_arrivals) show what is left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A pure move, so the next change to what a folder's writes do (#15798) fits
the file-size rule: Writer, Outstanding, write_records, queue,
start_writer, flush, active_chunk_writes, writes_finished, write_result and
SaveDir's Drop, unchanged but for pub(super) on the ones save.rs uses.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A far read of a saved chunk (far terrain's sampling threads, a player's
far ring) stays out of the gameplay region cache, so it opened the
region file for each chunk: 8 KiB of header read and parsed for a
record about as long (#6386: tens of gigabytes read under a far view).
The header is now kept, one a region (`RegionHeader`), and the file is
opened for the record alone. A kept header is only right until the
region is written: `RegionFile::write` moves the chunk and frees its
old sectors for the next chunk written. So each region has a write
stamp in the cache the writer goes through, odd while it is written. A
far read uses a kept header only if it was read under the stamp it
finds, and takes the stamp again after the read: unchanged, no write
began in between and the chunk was where the header says. Otherwise it
reads through the gameplay cache, under the writer's lock.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Replaces the opt-in native_mob_phase_quiet hook, which every existing mod
leaves unanswered, so any native mod kept the mob tick serial. The build
script now looks itself (horizon_mod_api_gen::mob_turn) and tells the
game through HORIZON_MOB_TURN:
- Mixins: the stage record names each injection's target. One into a
hook a mob's turn calls, or into anything but a native_* hook point,
keeps the loop serial.
- Trait mods: a staged trait package or its crates defining a function
named for a hook the turn waits on keeps it serial. Jump and damage
post are heard at the join, so overriding them doesn't.
A build without that finding but with trait mods stays serial, as before.
This also covers mixin-only bundles, which the opt-in hook never asked.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mob's turn calls native_trample (farmland) and the native_block_* contacts (a content mod's
blocks) from the movement code, on the group's own thread. Like the other hooks that hand a value
back, they are skipped in a grouped turn, and a quiet mod must not hook them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The parallel mob tick (#14457) no longer falls back to serial for pets,
raids, changed speeds, teleporting meals, withers, wardens, the tick
profiler or quiet native mods. Each case gets a bounded reach or joins
the mobs involved into one island:
- Pets: the owner's teleport destination box is a second place of the
pet's island, so it joins the mobs near its owner and none between.
- Raids: islands() already joins a raid's raiders; verify compares
mobs without the process-wide hurt counter.
- Speed, jump and flying speed: a per-mob locomotion bound from the
attributes and effects at their highest, derived in docs/mob-islands.md.
- Teleport consumables: half the item's TeleportRandomly diameter, and
the body reaches down as far as the mob can fall.
- Wither and warden: the ids they keep are links.
- Native mods: a new opt-in hook, native_mob_phase_quiet; while every
mod says it is quiet, grouped turns skip the hooks that hand a value
back and replay living_jump and damage_post at the join, in order.
- Tick profiler: each island thread samples into its own sums, merged
at the join.
native_hooks.rs may not grow, so mod_sound_ok, mod_particles_ok and
sound_id move unchanged to native_level_effects.rs, their main user.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the record no longer kept the ticks or changes the replay hadn't
played (a server that stalled for seconds), or a tick couldn't be played,
guest_replay dropped the replay and the check stayed off until the player's
next teleport. It now starts again from the server's own position for the
guest, excused until the moves agree with the client's again (at most 40
ticks), so the check resumes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
native_terrain_surface(dimension, columns) answers the generator's surface
height, top block, water surface and biome (ChunkGenerator::lod_surface) at up
to 4,096 columns without creating a chunk. A server player's /tp inside their
level to chunks that are not lit yet is held as a trip holds them, then placed
again once the chunks are in, so they no longer fall through the missing
terrain.
Fixes#15726Fixes#15727
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Full Performance sits before Performance: the lowest settings, far terrain off,
opaque ice and a 75% resolution scale. Every preset is now a fixed table, so
picking one after another leaves nothing of the first behind, and every one of
them turns on the optimizations that leave the picture as it is: asynchronous
entity and particle culling, the block-entity caches and GPU Culling's Auto.
The server tick's parallel phases were reachable only with HORIZON_PARALLEL_TICK,
read once at startup. Parallel Tick on the Performance page (parallelTick in
horizon-graphics.txt) switches them for single player and worlds open to LAN,
from the next tick, never during one. Auto keeps the tick on one thread until
its time on the processor is heavy for two windows of 100 ticks, shares it for
a trial, keeps sharing only if the median dropped, and goes back once the ticks
are light; a quiet world starts no helper thread. The variable wins over the
setting, so a dedicated server follows it as before. No preset sets it.
The console ran each command with no source, so it ran as the player in
place: `/me waves` said "* Player waves", `teammsg` looked for that player's
team, and relative coordinates (`~ ~ ~`) were that player's position. Vanilla's
console source (MinecraftServer.createCommandSourceStack) has no entity, is
at the world spawn's lower corner and is named "Server"; an RCON client's
(RconConsoleSource) is named "Rcon". as_console_in_overworld now runs the line
with the server's source (feedback heard, as command_harness's console does),
and say, me and the operators' line name it "Server" or "Rcon".
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerGamePacketListenerImpl.handleUseItemOn and handleUseItem call
ackBlockChangesUpTo(sequence) right after the client-loaded check, and it
throws IllegalArgumentException("Expected packet sequence nr >= 0") for a
negative number. The packet processor catches it and only logs "Failed to
handle packet ..., suppressing error": nothing else in the handler runs, the
idle timer is not reset and the player stays. Horizon passed such a packet to
the hub and the game, which used the item or the block. PlayConn::packet now
drops a UseItem or UseItemOn whose sequence is negative.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The relay pinged a host only after ping_every of quiet: every join line it
sent to the host restarted that wait. The host speaks on its control
connection only to answer a ping, so joins arriving less than ping_every
apart for host_timeout left it silent and the relay dropped a host that was
alive, retiring its code (later guests got `ended`). A ping now goes out
every ping_every whatever else is sent meanwhile.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`horizon-server backup auto` wrote `<time>-auto.zip`, byte for byte the name
of an automatic backup, so the next automatic backup's prune deleted it once
it was beyond `keep`, although backups made by hand are never deleted. `auto`
is now refused as a name for a hand backup, with a message saying why; the
automatic backups are named by auto_name, which no longer goes through that
check.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
BanListEntry and CachedUserNameToIdResolver parse their dates with
SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.ROOT). Its zone field
takes +hhmm with hours to 23 and minutes to 59, GMT alone or
GMT+h:mm / GMT-hh:mm, and UTC, in any case, and leaves what follows
unread. parse_date took any four digits after a sign and nothing else,
so "+2400" was a date and "... UTC" was not: an expiry written with UTC
or GMT became a permanent ban.
zone_offset reads the zone that way; the test's values are those
vanilla's own format gave.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla BlocksAttacks.disable, handlePlayerAction SWAP_ITEM_WITH_OFFHAND,
handleSetCarriedItem (main hand use), ServerPlayer.setGameMode (spectator)
and ServerPlayer.teleport (another dimension) call stopUsingItem, which
posts the item_interact_finish vibration. These paths only cleared the use.
Vanilla LivingEntity.updatingUsingItem calls stopUsingItem when the
hand's item is no longer the item the use began with, and
releaseUsingItem runs releaseUsing only for the same item (then
stopUsingItem). Remember the starting stack and do the same for the
host's and the guests' uses.
LivingEntity.aiStep ticks EntityEquipment, which calls
CompassItem.inventoryTick on each equipment stack: a tracker whose
lodestone is gone loses its target. Only the player and armor stands did
this; a mob keeps a compass pointing at a broken lodestone.
PiglinAi.stopHoldingOffHandItem deletes the off-hand gold ingot (without
bartering) only for an adult. A baby tries to equip it, then holds it in
its main hand. stop_admiring dropped it for any age.
LivingEntity.PLAYER_NOT_WEARING_DISGUISE_ITEM checks the helmet against
#gaze_disguise_equipment, which Enderman.isBeingStaredBy and the creaking
both use, not the carved pumpkin by name.
LivingEntity.causeFallDamage calls playBlockFallSound after the fall
sound: the block at floor(y - 0.2F) plays its fall sound at half volume
and three quarters pitch, unless it is air. The event was queued but no
one played it. Carry the block state and play it where the small and big
fall sounds already play: every other player on the server, and the
landing player's own client.
ExperienceOrb.repairPlayerItems takes EnchantmentHelper.getRandomItemWith
(REPAIR_WITH_XP, ...): one entry per enchantment that has that effect and
applies in the slot, not any item with the minecraft:mending id. A data
pack enchantment with the effect now repairs too.
ItemStack.canBeHurtBy reads the stack's damage_resistant component. The nether
star's #is_explosion came from a hard-coded item name, so other stacks with
the component (or a stack that removed it) were handled wrongly.
Entity.thunderHit calls ItemEntity.hurtServer for 5: the item loses health,
is destroyed at zero (onDestroyed spills contents), and is spared when its
damage_resistant covers lightning_bolt (ItemStack.canBeHurtBy). Share the
stack-aware resistance check the damage command already used.
EMF gives them in radians, the entity ones lerped within the tick
(EMFMath.getEntityRX/RY, getPlayerRX/RY). Degrees made a pack's
turn-in-place counter-rotation spin mobs round several times.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server copy of a guest's body never learned about leather boots, so its
movement check made a guest wearing them sink in powder snow
(PowderSnowBlock.canEntityWalkOnPowderSnow). Set the flag each tick like
the freeze-immune one.
PowderSnowBlock.getCollisionShape asks any entity for its fall distance,
the powder_snow_walkable_mobs tag and leather boots on its feet. Mob moves
passed that only for striders, so foxes, rabbits, endermites, silverfish
and mobs in leather boots sank in powder snow.
Vanilla LivingEntity.updateUsingItem calls completeUsingItem when the
remaining ticks reach 0 (BundleItem.getUseDuration is 200, no
useOnRelease), so the use sends entity event 9, runs the finish hook and
ends with stopUsingItem's interact vibration. The bundle only cleared the
use.
gfx.rs is over the 3000-line limit and may not grow, and the 4x terrain MSAA (#15479) adds to it. The impl block from after_submit to take_readback moves unchanged; the five private methods gfx.rs still calls become pub(super).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the current section mask in the tick loop and let callbacks signal reuse instead of cloning an Rc on every random-tick draw.
Co-Authored-By: Codex <[email protected]>
ServerCommonPacketListenerImpl.handleCookieResponse disconnects with
multiplayer.disconnect.unexpected_query_response, and the vanilla server asks
for no cookie, so every response is unexpected. Horizon decoded the packet and
let the game drop it. PlayConn::packet now answers it with that disconnect, for
the singleplayer owner too (a keep-alive has an owner exemption, this has none).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
PlayerDto reads its id with UUIDUtil.STRING_CODEC (UUID.fromString):
vanilla answers "Invalid UUID string" for
0123456789abcdef0123456789abcdef (UUIDUtil.LENIENT_CODEC, an int array
or that same string codec, refuses it too). Horizon's parse_uuid also
took the 32 undashed digits; now only the 8-4-4-4-12 form parses.
The test's 32-digit case moves to the refused ones. UUID.fromString's
other forms (five shorter dashed groups, a sign) stay refused here; that
is not in this change.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
block_s (the sampler's seconds per time block) is called only from the Linux
sampler, `mod linux` of server_perf/samples.rs, which is built for
target_os = "linux". On every other platform clippy with warnings denied
stopped on `function block_s is never used`. It now carries the same cfg as
its users, so Linux builds exactly what it built before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
BanPlayerCommands.banPlayers skips a player who isBanned, and
BanIpCommands.banIp refuses an address that is, so a second ban keeps the
first one and answers "Nothing changed". The console's own /ban and /ban-ip
relied on UserList::add, which replaces an entry whose reason differs: a
second ban with another reason replaced the first and said "Banned". They now
check isBanned first (Lists::ban and Lists::ban_ip), as the game's /ban does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An OperatorDto's permissionLevel and the value of
serversettings/operator_user_permission_level/set are decoded by
PermissionLevel.INT_CODEC: Codec.INT, then PermissionLevel.byId, whose
ByIdMap clamps. So 5 is 4, -1 is 0 and 2.7 is 2; only a non-number is
refused. Horizon answered invalid params for operators/add|set and an
error for the setter.
permission_level() reads it that way for both; the old test that
expected 5 to be refused now expects 4.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`restore NAME` takes "the name given to `backup` (the newest with it)", but
find matched any file name that ends with `-NAME.zip`, so `restore test`
took a newer `<stamp>-my-test.zip` over the `<stamp>-test.zip` it named, and
`restore auto` or `restore update` could take a hand-made `nightly-auto` or
a `before-update` backup. The label is the whole text between the time
stamp and `.zip`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
horizon-server init took its port and world name from a flag or answer and
wrote server.toml without checking them, so `--port 0` or `--world a/b`
gave "Wrote server.toml" and then every run failed to load it (Config::check
refuses port 0 and a world name with a slash, `..`, backups, logs or
.horizon). init now runs Config::check on what it will write, right after
the port is set and before anything else is written, and stops with that
message.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
PlayerList.op writes ServerOpListEntry(MinecraftServer.operatorUserPermissions()),
and DedicatedServer.operatorUserPermissions() is the op-permission-level
property. horizon-server's stopped `op` and `init --op` wrote level 4 whatever
the property said; they now write its level (0 to 4, 4 when unset), read as
the game reads it at its next start: server.properties with server.toml's
keys written over it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ServerGamePacketListenerImpl.tick kicks an idle player only when
!player.wonGame: a client showing the credits sends no action until it
asks to respawn. Horizon's kick_idle kicked such a player once
player-idle-timeout passed.
The decision moves into idles_out, which also takes the guest's
won_game, so it is tested without a running server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Pufferfish.tick` and `AbstractFish.aiStep` go through
`LivingEntity.makeSound`, which plays `Entity.playSound(sound,
getSoundVolume(), getVoicePitch())`: two random draws for the pitch, at the
entity's feet, and nothing when the entity is silent. The raw `Mob::sound`
helper played at eye height, at pitch 1.0, and ignored `Silent`, so a
`{Silent:1b}` pufferfish still blew up and out and a flop never varied.
`Pufferfish.playerTouch` sends `ClientboundGameEventPacket.PUFFER_FISH_STING`
only inside the `hurtServer` branch and only when the fish is not silent; the
sting for the player now comes from there, as a game event for a guest and as
the local play for the host, instead of the fish's AI tick playing it every
tenth tick whether or not any damage landed.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Flat and debug generator protos already contain their decorated columns, so expose a copy for lighting neighbours outside the kept area. This prevents the chunk pump from retrying impossible light-view jobs indefinitely.
Co-Authored-By: Codex <[email protected]>
docs/anticheat.md says an empty `log` writes no file, and no e2e case set
it. The new log-off case runs a survival server with `[anticheat] log = ""`
and a bot attacking without swinging: it is still kicked, the console still
has the [Anticheat] warning of the kick, and logs/anticheat.log is never
written.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecraftServer.tickServer counts a tick as empty only with no player
and no sprint running (ServerTickRateManager.isSprinting), and sets the
count back to 0 otherwise. Horizon counted sprinting ticks, so a sprint
on an empty server reached the pause almost at once and then stayed
stuck: the paused tick does not advance it.
The count moves into count_empty_tick, which also takes the sprint, so
the decision is tested without a running server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every timer test ran at 20 ticks a second, although Balance::tick_end takes
1000 / rate ms per client tick and the hub passes the server's rate. The new
test runs a client on time at /tick rate 10 and 60 for ten minutes (never
flagged), 1.5 times as fast (flagged again and again), and 20 ticks a second
on a server at 10 (flagged).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A wrong [anticheat] section makes horizon-server start and run refuse to
start (the error names the key); it is the game, reading server.toml
itself, that turns the anticheat off with an error.
- ground bans by default too (ban_after = 2), not only combat.
- A kick or a ban always reaches the console; the log line needs a log file
(log = "" writes none) and the operators' line needs alerts = true.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs/anticheat.md says a check's kicks are counted across sessions and
restarts, but check_combat bans on a second session of the same running
server. The new combat-restart case kicks a no-swing bot once, checks that
anticheat-strikes.json holds one combat strike, stops the server and starts
it again on the same folder, and expects the same bot, cheating again, to be
banned with the count back at 0. start() reuses a folder that is already
there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Config::check refused negative thresholds and a prediction tolerance of 0 or
less, but let any [anticheat.timer] tolerance_ms and max_lag_ms through. A
negative tolerance_ms flags a client on time every tick; a max_lag_ms below
-tolerance_ms does too, as the balance rests at the cap; NaN never flags, or
never caps the balance. Both are now numbers of milliseconds from 0 up,
checked by horizon-server before a start and by the game when it reads
server.toml.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
check_off ran only the timer check's 1.5x bot, so nothing showed that
`enabled = false` silences combat and ground, the two checks that drop, kick
and ban by default. It now also runs, on a survival server with allow-flight on
(vanilla's floating kick would end the no-fall bot on its own), the no-swing,
two-target and no-fall bots of check_combat and check_ground: none is flagged,
kicked or banned, no anticheat log is written, and their hits land.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tools/parity.py runs every tools/test_*_parity.py with no arguments. Six of
them are two-step proofs that need arguments (a Java side, then a native
side against its reference): builtin_component_delivery,
entity_movement_predicate, guest_close_event, machine_recipe_placement,
predicate_reference_startup and self_pose_metadata. Run bare, each exits
in 0 s with a usage error, so every parity-main run since they landed
(11-07 to 11-09) has failed on them, and the Nightly waits for a green
parity-main.
A script can now declare PARITY_MANUAL = "<how to run it>": parity.py
leaves it out of every run (--changed and parity-main included) and,
asked for it by name, says how to run it. The six declare it; nothing
else about them changes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fekd42tRMKJk8JighAmETj
In login_and_configure, while the server waits for LoginAcknowledged, a
CustomQueryAnswer or a CookieResponse was skipped and the login went on to
configuration. Vanilla's ServerLoginPacketListenerImpl.handleCustomQueryPacket
and handleCookieResponse both disconnect with
ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY
(multiplayer.disconnect.unexpected_query_response). The server sends no query
or cookie request at that point (its one query, the proxy forwarding request,
is sent and answered in forward() before LoginFinished), so any answer is
unexpected. It now sends the login disconnect through refuse(), logs the loss
of the connection and ends the login. Those two packets were the only ones the
wait skipped, so it no longer loops: it reads one packet.
Tests: a query answer and a cookie response each get the disconnect and the two
log lines; a login with nothing stray still reaches configuration.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`StonecutterScreen.containerChanged` (StonecutterScreen.java:205-209) resets
`scrollOffs` and `startIndex`, and the screen hangs it on the input container's
listener (StonecutterMenu.java:37-44), so it runs on any change of the input
slot: adding to the stack, a shift-click into it, and taking a result, which
uses one up (StonecutterMenu.java:66). Picking a recipe or clicking in the
player's inventory is not such a change.
Horizon reset only when the input was empty or there were no recipes, and ran
that after every menu session, so a cut left the list scrolled to its last row.
The reset now follows the input slot having changed, as the enchanting table
does, and a server's slot packet for the input runs it too
(AbstractContainerMenu.setItem -> Slot.set -> setChanged).
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merged note broke the two tests that read the geometry note; give
tessellation its own note and cover it in the post loader test.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Frustum.offset and Frustum.pointInFrustum are now ported (Frustum::pulled_back,
Frustum::contains_point), Frustum.isVisible and Frustum.cubeInFrustum cite the
file Frustum moved to (gfx/view_frustum.rs), and QuadParticleGroup.extractRenderState
names ParticleEngine::render_in_view, which does its frustum point test, so its
"no frustum point test" gap is gone.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
mob_to_nbt starts from the entity's earlier saved equipment and only
inserted the body; a body taken off (/item replace ... armor.body with air)
kept its old entry, so /data get still read it. Vanilla writes
equipment.body only while the slot holds something.
Fixes#12038
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
kacpherek's commit 061ba23407 made the frustum test read the particle's
position this tick instead of an interpolated one, but no test told the two
apart: the tests' particles all had previous equal to position.
The new test draws at partial 0.5 a particle that is ahead now and was far
behind a tick ago (kept) and one that is behind now and was ahead a tick ago
(dropped). With the test using the interpolated point instead, the first is
dropped and the test fails (left 0, right 4).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
ItemStack.isDamageableItem needs MAX_DAMAGE, DAMAGE and no UNBREAKABLE
(ItemStack.java:421-423); GrindstoneMenu's two input slots (GrindstoneMenu.java:50-52,
:56-58) and AnvilMenu's repair branches (AnvilMenu.java:133, :151, :157) use it. A
stick patched with max_damage but neither the item's durability default nor an
explicit damage is therefore not damageable, so it can no longer enter the
grindstone, nor merge into a "repaired" copy, nor combine its enchantments with another
such stack on the anvil.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HappyGhast.continuousHeal heals every 20 ticks when
`isInClouds() || precipitationAt(blockPosition()) != NONE`, but Horizon
only asked the precipitation, so a hurt ghast in clear air at the cloud
height healed on the 600-tick interval.
Entity.isInClouds reads the dimension's `visual/cloud_color` alpha (0:
no clouds at all) and `visual/cloud_height` (192.33 in the overworld)
and tests the entity's box against the four blocks above that height.
DimensionFacts now carries both, parsed from the dimension type's
attributes, and MobWorld::in_clouds ports the check; ai_step asks it
before the precipitation, in vanilla's order.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`decode_lod_surfaces` builds the far columns of a saved chunk. For each
of a section's 4096 blocks it looked the section's `palette` and `data`
up by name in the NBT, and for each block it kept the `biomes`, their
palette and data again: about half a million string-keyed map searches
a chunk, most of them over the air above the ground. That was 96% of a
far read of a saved chunk, on every far sampling thread (#6386).
Each section's block and biome indices are now unpacked once, as
`SimpleBitStorage.unpack` does, and a section whose palette holds
nothing far terrain keeps is skipped. The columns are the same.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
A new server hook, after native_explosion_detonate: an explosion is about
to hurt and push the entities in its reach, and a mod takes out the ids
to spare (NeoForge's ExplosionEvent.Detonate entity list). Spared
entities are neither hurt nor pushed. Players aren't listed; they keep
native_living_incoming_damage.
native_hooks.rs may not grow, so viewer_changes moves unchanged to
native_menus.rs, beside the menus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The ordinary-AI pufferfish rolls FishSwimGoal on its own random, which
both engines seed from the clock. About one run in ten it started
swimming in one engine only, and its position read failed (#15484).
A sync_randoms step before each fish's summon makes the roll repeat.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolves the docs/forking.md and docs/layers.md conflicts: main's rows kept, with game/extras/ in place of game/tweaks.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Entities::tick encoded an entity's packets once, then cloned every body into
each viewer's queue: one allocation and copy per body per viewer. Bodies now
travel as lan::Body (owned bytes, or an Arc<[u8]> several players share)
through the viewer queue, PlayerSender's held queue and the network threads'
outbox. Entity tracking (changes, pairing, pair_after_tick) encodes each body
once as a shared one; every other send keeps its own Vec without a new copy.
A test's channel (Out::Packets) still gets plain byte vectors. The bytes and
their order on the wire don't change.
Fixes#14922
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's Seen already carries invisible (flag or effect) and moved Mob::seen to
mobs/seen.rs; drop the duplicate field and put visibility_percent beside it so
mobs.rs does not grow.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
safe_name let a mod or pack file named CON, NUL.jar or COM1.zip through: on Windows the
joined path names a device, and the rename from its .part file fails or acts on it. It
also let bidirectional and zero-width marks through, so a shown name could read as
another. Both are now refused, like a path separator.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
RFC 6455 5.5 has control frames unfragmented with at most 125 bytes, and Netty's decoder
that vanilla uses fails the connection otherwise. Here a 64 KiB ping was accepted and
echoed, and a fragmented one answered. Both now end the connection.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Every handshake from a new address added a token, and the map was cleared only every 30 s,
so spoofed UDP handshakes grew it with the packet rate. At 65,536 addresses a new one gets no
answer until the next prune; an address already holding a token still gets a new one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
service install wrote the server folder and the program path into the unit as they were. A %
in a folder name is a systemd specifier, a line break adds a directive, and a quote or a
backslash changes ExecStart's quoting. Such a path is now refused with a plain message.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
prune_chunks.spawn_radius is a u32 cast to i32, so a value above i32::MAX wrapped negative,
no chunk counted as near spawn and a delete run pruned the spawn area. The radius now
saturates at i32::MAX: as large a radius as asked for, keeping every chunk.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Failed logins were counted for everyone and checked before the code, so ten bad /login
requests a minute from anyone who could reach the panel made the owner's own link fail with
429. The limit now applies only to wrong codes; a valid link always signs in.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
vanilla_client picks the natives jar the launcher's rules keep for this
OS and architecture (natives-windows, natives-windows-arm64, ...) from the
version JSON instead of knowing only macOS and Linux. The JVM writes UTF-8
and the reader replaces stray bytes, so a cp1252/cp850 console no longer
kills it; the log is UTF-8. lab.binary finds horizon-client.exe.
Fixes#15500
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A program with a .tcs or .tes stage was loaded as a plain vertex and
fragment program, silently dropping the tessellation stage. Horizon has
no tessellation backend, so skip such a program with a note, as an
unsupported geometry stage already is, and let the fallback chain pick
the next program.
Fixes#7922
compare_answered compared the two clients' answer ticks whole. In dig_teleport
(`tp @s ~ ~ ~`) the answer moves nobody, and LocalPlayer.sendPosition's 20-tick
position reminder stays on the script's clock (ClientPacketListener.handleMovePlayer
resets neither positionReminder nor xLast): it falls on tick 27 in both clients.
When the server answers Horizon a tick later than vanilla (26 vs 27), Horizon's
answer tick carries the reminder and vanilla's doesn't, which the nightly reported
as an extra move_player_pos (#14807).
The answer ticks now compare without their moves, and their moves compare tick for
tick, on the script's clock.
Fixes#14807
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keeps both new tests at the end of hub_tests.rs (main's teleport accept test and the equipment cache test).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An effect's colour comes from the resource packs (set_pack_colors) and a
pack reload replaces it without replacing the hub's SyncData, so a cached
particle kept the old colour. The cache now holds only the registry id of
each particle type; the colour is read on every call. New test: a pack
colour change shows although the id is cached.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This pull request goes back to the 26.2 registry fix it was opened for
(#15138). The ten follow-up commits that landed here are open as smaller
pull requests of their own: #15430 (entity data serializer ids), #15431
(equipment in the client's ids), #15432 (1.21.2/1.21.3 step, dust colours,
attribute UUIDs) and #15433 (own core packs and registries for 1.20.5 to
26.2, #15143); mannequins and player_loaded follow after those.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The stack dump test asked for the worker's stack as soon as the worker's
handle arrived. The worker could still be inside the channel send that
woke the test thread, so the sample sometimes held no busy frame and the
test failed about one verify run in three.
creatures.rs may not grow after main moved the mob loop out; end_mob clears the mob's context, Projectile::tick sets its own, and the fangs start with none.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
gfx.rs is over the 3000-line limit and may not grow, and the 4x terrain MSAA work adds to it. The impl block from resident_sections to cpu_census moves unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
after_handler is only called on the signal path (Linux, Android, Apple),
and the tests' busy thread only serves the test that reads another
thread's stack there: on Windows clippy -D warnings stopped in
horizon-threads, and every build warned about after_handler. The function
takes the same cfg_attr(.., allow(dead_code)) as Watched::thread, and the
test helpers and their imports the test's cfg.
Fixes#15476.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved Hub::effect_meta into SyncData::effect_meta (read by helper
threads since #14506). The per-(effect, ambient) particle cache follows it:
lan/effect_meta.rs keeps ParticleCache and particle_of, SyncData holds the
cache behind a Mutex and Hub::effect_meta delegates as on main. hub.rs
loses the field and module line instead of growing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
crates/horizon-client-render/src/gfx.rs: this branch moved Frustum into
gfx/view_frustum.rs (kept, with its mod and pub use); main left Frustum as it was
but moved shadow_box into gfx/external.rs, so the old shadow_box beside Frustum
here is dropped.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Each server runs with its temporary world as the working directory, so a relative
`--binary target/fast/horizon-client` was looked up under the world and failed even when the
binary existed. Make the path absolute before starting the servers, with a unit test.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Its gzip size depends on the flate2 backend that feature unification picks
(3.7 MB alone, 5.2 MB in a workspace test run); what matters is that the
announced-size cap lets it through, so the inflated cap is what stops it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On main `horizon-server report` printed the report's text summary; with the
capture it printed only the file's path, and the text form moved to
horizon-perf, which no release ships, so a host on a VPS could not read the
result there. horizon-perf-format, already shipped through the platform,
gets server_brief: the kind and length, TPS and MSPT, CPU, RSS, players,
chunks and entities, and the five heaviest tick phases. The report prints
it before the path.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Any accept() error other than WouldBlock ended the server that offers a
capture to the viewer, before the viewer had fetched it: an aborted
connection (ECONNABORTED, common on BSDs and macOS, WSAECONNRESET on
Windows) or a full file table (EMFILE) left Open broken for that click.
Errors now wait 10 ms and try again until the offer's deadline or a cancel.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A thread's ring was sized at its first session and kept, but the size a
session needs depends on its Hz, --wall (twice the entries) and the unwind
mode (about 16 KiB per stack copy), so a later capture with --wall or a
higher --hz lost most of its samples to `dropped` (95 of 99 in the new
test). A session now gets a new, larger ring when the thread's is smaller
than it needs. A replaced ring stays allocated for the thread's life, since
a late signal's handler may still hold it; rings only ever grow, so this is
at most a few per thread.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The requester was keyed by connection id and never cleared on disconnect,
so after the admin's client dropped during an open-ended `profile server
start` everyone got AlreadyRunning until the 600 s window ended, and the
same admin, reconnected under a new id, got NotRunning when stopping. Each
tick's requests now include a stop (reason DISCONNECTED) once the
requester's connection is gone; the file stays in the server's reports/ and
its path is logged, and the slot frees as soon as it is saved, so anyone
(the same admin included) can start again. server_profile_e2e covers the
disconnect and the start after reconnecting.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
share_saved always reused an existing .hzperf.share.json and only re-pinned
it, so once the upload expired (a temporary one after an hour, a shared one
after 30 days) or was deleted, Share failed for that capture until the
receipt was removed by hand. A 404 or 410 on the pin of a reused receipt now
uploads the capture again, replaces the receipt (still 0600, written beside
and renamed over) and pins the new upload.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
with_profiler keeps a server's or mod's own `profiler` node, so the chat
completed the server's command while handle_chat_input still answered
/profiler locally, and the server's command could not be reached. Horizon's
nodes now carry their own command id; a typed /profiler runs locally only
when the chat tree's `profiler` is Horizon's (or there is no server), and
otherwise goes to the server or mod that owns the name. The client's own
buttons keep running locally.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
click_command answered any clicked /profiler line itself, so a server's
chat component, book or dialog with run_command "/profiler share" uploaded
the player's last capture after one click. The client's own [Open],
[Share], [Start], [Stop] and retry buttons now carry a client-only click
action that no server text decodes to (as [Folder]'s open_file already
was); a clicked run_command is a plain command again, sent to the server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The step's /execute if loaded read the spot's own chunk. The Java oracle's
player isn't ticked, so its tickets stay on the 5x5 chunks around its
spawn, while Horizon's player loads the chunks around wherever it stands:
once GameTestRunner's batch listener released the forced chunks, spots 56
to 104 were unloaded in Java only (4 of 99 records, #15158). The read is
now of chunk x 11, which only /forceload holds and neither player reaches;
both sides see the release at the same step.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The merger was taken out before finish(), so when finish() failed the
abandoned part was closed with a fresh merger that had yielded nothing,
and no Stop{DISCONNECTED} was written for a part that began. finish() now
borrows the merger, which keeps what it yielded for cut_short.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client took whatever size a server announced and inflated every chunk
without limit, and appended fields of any side into the player's capture.
The announced size is now capped at 64 MiB (the share endpoint's own cap),
the inflated part at 512 MiB (the timeline's 256 MiB cap plus the rest of
a 600 s capture; real captures inflate about 3x from 130-230 KB a minute),
and one field at 64 MiB, which also bounds what waits half-arrived. A field
whose message is not side = SERVER, or has no side (an attachment), refuses
the part; what arrived stays with the incomplete warning, as for any part
that fails.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Address Akirabane's small follow-up suggestion on PR #14257: pin floor and saturation when a crafter's triggered value loads from a double NBT tag. No production behavior changes.
Co-Authored-By: OpenCode (GPT 6.1 Sol) <[email protected]>
- allay_pickup_cooldown passes on main now (twice locally with prepared
resources), so it leaves named_only in parity_hosts.json (#15158).
- allays_heal_on_tenth_ticks_without_enabling_noai_work still set and
read extra.ints[3], which held ITEM_PICKUP_COOLDOWN_TICKS until it
moved to AllayState::pickup_cooldown: the test fails on main whenever
prepared resources are present (CI has none, so it skips). It now
sets and reads the cooldown through allay::set_pickup_cooldown and
pickup_cooldown.
Part of #15158
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its parity run failed boat_fire (781 of 789 commands): after a boat burns, Horizon leaves an item where vanilla leaves none. The harness does not fail on main without it. Merged by mistake before the author answered; it can come back once boat_fire passes.
game.rs, title.rs and save.rs are over 3000 lines and may not grow: the Alt state lives in
WorldOptions, set from update_modifiers, and WorldPreset::step sits in create_world.rs.
Address kacpherek's request on PR #14365: remove the post-hit tie scan while keeping slice-based search and the short-list upper bound. Extend the existing regression test with 16-entry duplicate and signed-zero lists; expected indices were observed on the JVM and the pinned vanilla method was checked with javap.
Validation: cargo check -p horizon-assets --locked; cargo test -p horizon-assets --locked (53 passed); cargo fmt -p horizon-assets -- --check; cargo clippy -p horizon-assets --all-targets --locked -- -D warnings.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Resolve the shared first_json helper conflict by retaining the generic parser while preserving main's metadata behavior and animation regression test.
Co-Authored-By: OpenAI GPT-6.1 Sol <[email protected]>
Address the reviewer request by retaining main's invulnerable_time field rename alongside the PR's RNG changes.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
The attack-checks test only asserted that attacking a pig does not
disconnect the guest. It now also asserts that the pig lost health,
so a normal attack on a mob is shown to still land.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
open_uri on Linux and macOS went through open_folder, which now resolves
the path first, so a link would fail as a missing file: desktop::open_uri
hands it to open / xdg-open as it is. dunce leaves a share (a mapped drive
too) or a path past MAX_PATH as \?\ (UNC); file_uri gave file://%3F/...
for those, which the shell can't open. dunce and open leave horizon-client,
which no longer uses them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla 26.3's oit_sample.glsl sizes an array by a constant declared in
its function (const int targetCount = OIT_COEFF_COUNT / 4;
vec4 coeffSamples[targetCount];), which naga's GLSL front end refuses.
Each such array size takes the constant's expression, from the
innermost scope that declares it. A guarded include pasted twice
declares the same constant twice and still sizes its arrays; two
different expressions for one name in a scope, or more than 256
constants, leave the text as it was.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Blaze3D.openPath hands path.toUri() to SDL_OpenURL, which on Windows is
ShellExecute's open verb, so a folder opens in the default file manager.
The pack folders (packs::open_folder) and the world and backup folders
(world_management::open_folder) started explorer instead. Both now go
through desktop::open_path: a file: URI to ShellExecute on Windows (the
open crate's folder shortcut, SHOpenFolderAndSelectItems, is Explorer's
own), open / xdg-open elsewhere.
Fixes#15259.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Create World type button never reached Debug Mode. Vanilla cycles WorldPresetTags.EXTENDED (normal list plus Debug) while Alt is held, and the normal list otherwise.
The sound was picked from the weapon on every hit and saved as item.crossbow.hit forever. Vanilla's soundEvent is the crossbow's only until onHitBlock sets entity.arrow.hit.
The device-code interval, its expiry and the Minecraft token lifetime came from the services
unchecked; Instant + a huge interval panicked the sign-in thread and left the window on the
code. The interval is clamped to 1-60 s (slow-down too), the device code's expiry to an hour
and the token lifetime to a week, all added with saturating_add.
Split out of #15247 at review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
- 1.21.2 and 1.21.3 had no step of their own after 1.21.4's: level_particles kept 1.21.4's always_show and
player_info_update its UPDATE_HAT (bit 7, checked against the jar's Action order); both go now.
- Dust and dust_color_transition colors are written as three floats for clients before 1.21.2 (only reading
knew that form).
- update_attributes names modifiers by UUID for 1.20.5 and 1.20.6 (vanilla's old UUIDs, else
nameUUIDFromBytes of the identifier).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 8245a5040068681e6e55156a953c0630690cfa42)
1.21.6's step (the saddle slot) read set_equipment, already in the client's item and component ids, as pinned
ids, so the first equipment with a component disconnected every client through 1.21.6 ("NBT at byte 1: unknown
tag type"). It reads and writes the stacks in the client's ids now; a session test sends equipment with
components to every older release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2dc378ce5b05a6dbb8dad635490e0fc86d73d183)
A compressed frame declares its uncompressed size up front, and the inflater reserved that
much at once: a few bytes declaring 8 MiB made the reader allocate 8 MiB per frame. The
buffer now grows as output arrives (capped by DEFLATE's ~1032:1 ratio), so a lying frame
is refused after a small allocation and a real large body still inflates whole.
Split out of #15249 at review.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
ServerGamePacketListenerImpl.handleAttack disconnects a player whose
attack names an ItemEntity, an ExperienceOrb, the player itself or an
AbstractArrow that is not isAttackable, with
multiplayer.disconnect.invalid_entity_attacked, after the world border,
attack range and PIERCING_WEAPON checks. A hosted or dedicated Horizon
server let the attack end without a word.
handleAttack's checks before Player.attack move to
game/lan_actions/attack_checks.rs: the border and range checks
guest_attack already did, then that test. An arrow's isAttackable reads
#minecraft:redirectable_projectile from the tags the players were sent,
so a data pack that adds arrows to it keeps them attackable.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A client before 1.21.4 has no player_loaded, so the server never counted it as loaded: its per-tick player work
(command suggestions, chat throttling, pickups) never ran. After its login and each respawn, the first teleport
it accepts is followed by a player_loaded (the serverbound packets a translation adds now queue in order).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit da41f81714e7370bb0637b84e74519af368fbd2c)
- A client through 1.21.8 has no mannequin (1.21.9): it is shown a player under the mannequin's entity id and
UUID with its skin (a player info entry, ADD_PLAYER only, empty name, removed with the entity). The add waits
for the first entity data (the profile); any other packet first shows the waiting ones without a skin. Of the
entity data the player keeps Entity, LivingEntity and Avatar's (up to the skin layers). One server packet can
now become several (ClientSession::clientbound_all, ConnWriter::for_client).
- 1.20.6's level_particles is laid out as 1.21's (the particle after the count): the 1.20.6 client jar's
ClientboundLevelParticlesPacket reads flag, position, spread, speed, count, then the particle. The step that
moved the particle first (and its test-side mirror) is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit d0d014fe65900fa8f72e46db6ae13dbf44598a69)
- 1.21.2 and 1.21.3 had no step of their own after 1.21.4's: level_particles kept 1.21.4's always_show and
player_info_update its UPDATE_HAT (bit 7, checked against the jar's Action order); both go now.
- Dust and dust_color_transition colors are written as three floats for clients before 1.21.2 (only reading
knew that form).
- update_attributes names modifiers by UUID for 1.20.5 and 1.20.6 (vanilla's old UUIDs, else
nameUUIDFromBytes of the identifier).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 8245a5040068681e6e55156a953c0630690cfa42)
1.21.6's step (the saddle slot) read set_equipment, already in the client's item and component ids, as pinned
ids, so the first equipment with a component disconnected every client through 1.21.6 ("NBT at byte 1: unknown
tag type"). It reads and writes the stacks in the client's ids now; a session test sends equipment with
components to every older release.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2dc378ce5b05a6dbb8dad635490e0fc86d73d183)
Generalises the 26.2 known-pack fix to every release with known packs (1.20.5 up): the client is offered
minecraft:core in each release name of its protocol; synchronised registries it doesn't have are not sent
(and their tags not either); entries its release lacks leave their registry (tables generated from the
vanilla jars, tools/generate_sync_registry_adapter.py), and the ids into those registries are renumbered:
chunk biomes, tags, damage_event, item enchantments / trims / banner layers / painting variants, painting
entity data. A client before 1.21.2 has no client_tick_end: one follows each of its movements, so its
second step is no protocol error. Clientbound translation errors name the packet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit de96547a9b0a3a0327bad39559c6ab7d5ba6111c)
Up to 26.2 a client follows accept_teleportation with a move_player_pos_rot to the same place;
26.3's accept carries the place, and a second move in one client tick is kicked as invalid
movement. A real 26.2 client was kicked right after joining (several teleports at once).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 7366ea174632b2dc13307977589996a372185505)
The pass timing switches and readings and the world pass helpers move
from gfx.rs to gfx/pass_timing_api.rs, the streamer's queue census and
job_stats to streaming/, Horizon's mark sprite to mark_sprite.rs, and
`horizon-ctl lan op` to game/lan_op.rs. A guest's plugin channel
routing moves beside horizon_channel, the client takes the profile
channel's replies before handle_packet, and tick_profile starts and
ends a mob's tick for both the sampler and the mob costs. The server
bench test reads the saved line the server prints now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Removes the fork's review and claim tooling and its docs, and the scene
corpus, which the design keeps separate (an input fixture, not a
measurement). The design doc describes the viewer and upload API in
horizon-rust/horizon-site and drops the planning sections; tests and
doc comments point at playhorizon.dev and horizon-site. The debug keys'
comment is upstream's again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A reader skips a version broken on the player's Horizon commit for an
older one that builds; only downloads, stars and updated are for show and
sort. None of the four is checked against a downloaded file.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`[shadow]`, `[[lights]]` and `[[particle_lights]]` were refused without a
`[material]` (or, for the shadow, a `[[post]]`), though the entity stage reads
the shadow maps and the coloured lights too. A 0.2 pack with only `[entity]`
had to add a stage it didn't want. The three checks now take `[entity]` as a
stage that reads them; 0.1 keeps its rules, and its two light messages are
reworded with the same meaning.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
`projects[].updated` is the date of a project's newest version, set by the
registry's publish job (horizon-rust/horizon-mods#683). While the index is
preview, a reader may rely on every field but the publish job's four
(`downloads`, `stars`, `updated`, `broken_on`), which may be absent at any
time. Point (3) of #10562.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Preserve the contributor's renderer extraction unchanged while including main's fix for the Git maintenance race in test_git_slim. The diff against main remains the same four moved methods and port-map citation.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Includes main fix 04b53013c8 without unrelated local code edits; preserves the contributor history and translation-only PR diff.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
High-framerate gameplay (e.g. 144Hz - 300+ FPS) suffers from micro-stutters
when chunk mesh uploads are capped by a flat 2 ms time budget, because 2 ms
can represent 30% to 60% of an entire frame interval (which is ~3.3 ms to
~6.9 ms).
This commit introduces dynamic frame-paced upload budgeting:
- Computes an exponentially smoothed frame time.
- Budgets mesh uploads dynamically to at most 25% of recent frame time.
- Enforces a 250 µs floor and scales up to a 2 ms ceiling when a backlog
of pending uploads accumulates.
- Preserves the higher 6 ms allocation during initial terrain arrival
(world open or teleportation).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EditBox.handleCursor requests CursorTypes.IBEAM while hovered and editable, and
NOT_ALLOWED when it takes no text. Horizon's cursor set had no I-beam, and the
pack screen's search boxes and the settings shell's search never asked for one.
widgets::text_cursor adds it, the two screens call it where their fields are
drawn, and the client maps Cursor::Ibeam to CursorIcon::Text.
Resolve the corpus registration conflict without changing any main group or hanging-config operation. Restore the BoatItem.use documentation to place_boat.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Preserve the contributor's TNT power change and the minecart extraction already on main. Git resolved both touched files without manual conflict edits. The net change against main is three files (+82/-2).
Validation: rustfmt, public-tree, layer/file-size checks and three command-fixture Python tests pass. The focused Rust test attempt failed in sccache before compiling Horizon; vanilla resources and the port ledger are unavailable, so gameplay proof remains outstanding.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
`a_full_hopper_cannot_take_a_book_from_a_chiseled_shelf` asks the rule
with a hopper full of stone (false) and an empty one (true). Without the
shelf rule both answer true.
`ChiseledBookShelfBlockEntity.canTakeItem` is
`target.hasAnyMatching(other -> other.isEmpty() || (same item and
components && other.count + stack.count <= target.getMaxStackSize(other)))`:
a hopper may take a book only when it has a free slot or a matching stack
with room.
Horizon's `Containers::can_take` had no shelf rule, so a full hopper
still pulled a book out (and put it back). The hopper's path now asks
`can_take_into`, which applies the shelf rule before the existing checks.
Without the 26.2 layouts switched on, decorated_pot_pattern, block_transformer and
worldgen/block_state_provider still reached 26.2 clients; they go with the known-pack path now.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A 26.2 client only has minecraft:core 26.2, so the pinned offer (26.3) was answered with no
known pack and every synchronised registry went out as 26.3 data, which 26.2's codecs can't read
(e.g. the dimension type attribute minecraft:gameplay/straw_bed_rule): 'Failed to load registries'.
- select_known_packs: the client is offered core in its release, its answer goes back up as the
pinned pack, so registry entries go without data as for a pinned client.
- worldgen/biome: the entry 26.2 lacks (dappled_forest) leaves the registry; biome ids in chunk
sections, chunks_biomes and biome tags take the client's numbering (dappled_forest -> forest).
Only 26.2 for now: the synchronised registries of the 26.2 and 26.3 jars were compared; older
releases lack more entries and keep today's path.
The row said "scaled by visibility" while the code used the two extremes of
the invisibility effect; it now names `getVisibilityPercent`, the flag or the
effect, and the 0.07 factor.
Two evoker wololo casts in a real goal selector, with a blue sheep 14 blocks
away: the visible sheep starts `UseSpell`, the same sheep with `invisible: true`
does not. Without the visibility-scaled range both are picked, `[true, true]`
against `[true, false]`.
The existing five-block test with the invisibility effect still passes: the
effect counts beside the flag.
`Evoker$EvokerWololoSpellGoal` finds its blue sheep with
`TargetingConditions.forNonCombat().range(16)`, and `TargetingConditions.test`
scales that range by `LivingEntity.getVisibilityPercent` with a floor of 2.
Horizon asked `has_effect("invisibility")` and used the two extremes, so a
sheep with the `invisible` entity flag and no effect stayed in range at
sixteen blocks where vanilla's percent is 0.07 and the range 1.12, floored to
2.
`Seen` now carries `invisible` (`Entity.isInvisible`) and answers
`visibility_percent`, the mob half of `getVisibilityPercent`: 1, or
0.7 x armor cover (at least 0.1) while invisible - a mob does not sneak and its
`getArmorCoverPercentage` is 0, so 0.07. The effect counts beside the flag
because vanilla's `updateInvisibilityStatus` copies it into the flag each tick
and Horizon has no port of that yet. The wololo filter uses it.
`Seen.invisible` is the same field #13755 adds for follow-mob goals; whichever
lands second takes the other's line.
Vanilla's predicate is `state.getFluidState().is(FluidTags.WATER)`, which
the inline 9x2x9 search already matches; the row's `canBeHydrated` gap
was wrong.
- Button labels are horizon.touch.* keys looked up at draw time; F3, F5, II and ... stay literal
- English text for the new keys in en_us.json; other languages fall back to English
- Tests pin the rgba byte order, the pack's highlighted button sprite, the no-sprite frame, label shrinking and the D-pad and knob direction
- D-pad cells and knob position move into small functions so the tests can reach them
- The label scale names its width fit and height cap
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- TouchControls::draw puts every visible control on the HudBatch at its placement and opacity
- Buttons use the resource pack's widget/button sprite (highlighted while held) with a short label that follows the player (Up/Down when swimming or flying, Dismount when riding); a plain frame stands in when the pack has none
- The joystick is a rim and a knob, the D-pad four arrow buttons; a dynamic joystick only shows while held
- The tap schemes draw a touch marker that fills outwards while mining (break_progress)
- No art ships with the game: Starling's Bedrock control textures are not used
- Settings gain circle_size (16-64, serde default 32)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ThrownEgg.onHit sets the chicken's variant from the item's CHICKEN_VARIANT component (egg temperate, blue_egg cold, brown_egg warm); a thrown or dispensed egg now carries it.
A native explosion inside a TNT blast is Level.explode(null, ...): it
now takes blast_owner, blast_guest and blast_owner_player aside with
the rest of the blast's state. The group keeps its zombie persistent
and clears the zombie's drops.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
decode_block_entity reads every int through nbt_path::int_value
(NumericTag.intValue: a float or double floored and saturated), so the
crafter's triggered keeps rustfmt's single line and save.rs does not
grow. The crafter group moves to a chunk no other group forces within
five columns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ground_from_nbt on main already reads OnGround and fall_distance, so the
helper this pull request added goes. TeleportCommand.performTeleport
sets onGround. Powder snow's inside shape for a fall over 2.5 is 0.9
tall, so a crystal above it keeps its fall. The deflate is 1.0E-5F.
The group adds both powder snow cases and a teleported crystal.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CrafterScreen asks menu.isPowered(), data slot 9 == 1, not the block
state, so a crafter[triggered=true]{triggered:5} shows the unpowered
arrow. getIntOr reads a double through DoubleTag.intValue, floored and
saturated: -0.5d is -1 and 4294967297d is 2147483647, where the cast
truncated and wrapped.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The loaded OnGround and fall_distance now live on the crystal, so a
crystal summoned where a loaded one was killed starts from nothing and
a teleported crystal keeps its own (the position-keyed compound found
neither). EndCrystal.tick's applyEffectsFromBlocks resets the fall in
a cobweb or powder snow (makeStuckInBlock). Crystal::new builds the
fresh crystal its five call sites wrote out; start_time moves beside it
in dragon/crystal.rs, keeping dragon.rs from growing.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
PrimedTnt.getOwner names the mob a saved owner UUID points at, so its
blast's indirect source is no player and tryAwardExperience awards
nothing. Keep whether the owner is a player beside blast_owner (a chain
reaction hands a mob owner on as a mob). Block.popExperience draws the
amount, then pops nothing with block_drops off.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`Fox.aiStep` finishes the item in the mouth with `ItemStack.finishUsingItem`
(`Consumable.onConsume`), which ran only the chorus fruit's teleport here: run
the whole consume in vanilla's order — `emitParticlesAndSounds` (the
consumable's sound, and the four draws a crumb of `spawnItemParticles` costs,
though `Level.addParticle` is empty on a server), `FoodProperties.onConsume`'s
own sound, the `on_consume_effects` through `LivingEntity.addEffect` (so a
golden apple's regeneration and absorption land) and `GameEvent.EAT` at the
fox's feet with the fox as its source.
`Panda.pickUpItem` calls `onItemPickup` before it takes the item, so
`CriteriaTriggers.THROWN_ITEM_PICKED_UP_BY_ENTITY` fires for whoever threw it,
as it already did for the fox (`LivingEntity.onItemPickup`).
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.load reads NoGravity, Silent, Glowing and the other flags with
getBooleanOr and Entity.saveWithoutId writes each back as 1b only when
true. Horizon saved a leash knot's, a marker's or an eye of ender's raw
compound, keeping bytes like -1b and false flags; other_to_nbt now
normalizes them as the other entities' saves do.
Fixes#13853
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vex.tick calls setNoGravity(true) on every tick, AI or not, so a vex that
ticked once saves NoGravity: 1b. Horizon's vex flew through its own
travel without the flag and left it out of the save.
Fixes#13849
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CreakingHeartBlock.onExplosionHit awards the heart's experience
(tryAwardExperience) when the explosion's indirect source is a player, a
primed TNT's owner, unless that player is in creative or spectator
(preventsBlockDrops). Horizon always broke the heart as if no player
caused it.
Part of #11330
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.load reads OnGround and fall_distance and Entity.saveWithoutId
writes them back. BlockAttachedEntity.tick never moves a painting nor runs
baseTick, so both stay what was loaded. painting_to_nbt wrote OnGround 0b
and fall_distance 0; it now writes the loaded values from the painting's
saved compound.
Fixes#14543
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.load reads OnGround and fall_distance and Entity.saveWithoutId
writes them back. EndCrystal.tick never moves the crystal nor runs
baseTick, so both stay what was loaded. Crystal::to_nbt wrote OnGround 0b
and fall_distance 0; it now writes the values from the crystal's loaded
compound.
Fixes#14671
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The round trip at 1 passed with the old bool as well; values other than 1 come back
unchanged and untriggered, as vanilla saves and reads them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CrafterBlockEntity keeps triggered as the int it read (containerData
slot 9), writes it back and is triggered only when it is 1. Horizon held
a bool read as != 0, so -1 or 5 became triggered and saved as 1. The
field is the raw int now, with is_triggered() for the bool.
Fixes#13851
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The e2e capture's live heap was all on horizon-profile: the CPU
sampler's drain thread copies the binary's unwind sections (~9 MB,
framehop's Module::new from StackUnwinder::refresh) at the start of the
capture and holds them to the end, so the heap sampler profiled the
profiler. Samples were already tagged with the allocating thread.
heap_sample::ignore_this_thread() stops sampling a thread's allocations
(its frees still leave the live set). The sampler's drain thread, the
capture writer (WriterThread::spawn_with), the finishing thread and
the gauges thread call it.
server_perf_e2e checks live bytes over every thread, none on a profiler
thread, and that the top live function is not the profiler's; it
samples every 64 KiB so its 4 s capture holds some. It now passes: 1.0
MiB live on tick, workers and chunk threads, 19,453 samples.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Chat keeps the lines with a next step (started, saved, shared, failures); saving,
uploading, opened and marked go to the action bar; the saved line carries the
capture's length from when stop was asked; [Retry] names its profile.
Text::styled dropped the style's inline object (Style::inherit never passes one
on), so the profiler tag drew the font's [OBJ] box instead of horizon:gui/mark.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Avoid shadowing the ownership map with the layer-key list so the latest main branch compiles after the CEM armour-layer merge.
Co-Authored-By: Codex <[email protected]>
By rdbtCVS's AI agent (OpenCode, GPT 6.1 Sol). Preserve the contributor and maintainer history while including main's merged #15027 repair for the inherited studio_replay and rocket RNG compilation failures. No PR-specific code changes.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
Apply the burst damage-calculator multiplier to the Ender Dragon impulse before native knockback hooks, and cover the behavior with a regression test.
Co-Authored-By: Codex <[email protected]>
Use the production timeout builder rather than an unrelated test agent, and
cover IPv4-first multi-address and IPv6-only resolver ordering.
Fixup by rdbtCVS's AI agent (OpenCode, GPT 6.1 Sol), not by rdbtCVS personally.
Co-Authored-By: GPT 6.1 Sol <[email protected]>
`horizon-ctl memory` and a capture's memory snapshots are built from `#[derive(Serialize)]` structs instead
of `json!`; existing keys keep their shape. A dedicated server (no window) names its columns `columns`;
single player keeps `client_columns` and `server_columns`.
New, all estimated from capacities:
- `entities`: count and bytes by entity type (top 30), across every level
- `players`: count and, per player by name, the send queue (packets and bytes), chunks waiting, chunks sent
and tracked entities
- `chunk_send`: the encoded section and heightmap caches and the shared compressed frames
- `light_queue`, `generation_queue`: lengths and the bytes of their tables
- `allocator`: `live_heap_mb` (mimalloc's statistics), `commit_mb`, `peak_commit_mb`, `overhead_mb`
- `named_mb` and `unattributed_mb` (resident minus the named parts) on every side
The server e2e asserts a stop snapshot has `players`, `entities` and `unattributed_mb`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`hot` is the top 30 functions (the `hot` op's row) of the main thread's samples in the time blocks
overlapping the tick, only the slow samples when the tick is slow, and `samples_from`/`samples_to` say the
window those blocks cover. With 1 s blocks that is the second around the tick, not the tick's own samples.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`memory_labels` (CLI `query memory-labels`, MCP and wasm through the shared op list) gives one row per
snapshot, `{label, t, side, rows}`, in time order. `counters` gives one row per counter definition,
`{id, name, unit, kind, labels, side, samples}`. Each has a golden reply. `memory` also ranks a negative
megabyte row by its size, so `unattributed_mb` can show.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`block_s` defaults to 1.0 and `MAX_TIME_BLOCKS` is 1800 (30 minutes, the block index a `u16`); a longer
session keeps the rest in the last block, as before. The reader selects every block a `--from`/`--to`
range overlaps, a range inside one block picks that block, and the reply's `selection.from`/`to` name the
blocks the weights cover, so a 2 s range no longer leaves Functions and the flame graph empty.
Capture size before and after on the 200-bot benchmark and a client capture is not measured yet: pending a
quiet machine. If growth is over 2x, blocks older than 5 minutes get merged at write time.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
horizon-threads' heap_sample wraps mimalloc and is the game's allocator
on Linux (alloc-count and alloc-census still replace it). While a
capture runs it samples about one allocation per 512 KiB
(HORIZON_HEAP_SAMPLE_KIB; 0 turns it off) with an exponential countdown
per thread, records the stack, thread and span context, and weighs
each sample interval / (1 - exp(-size / interval)) bytes. Frees look a
pointer up in a 1 MiB table of per-bucket counts and only take the lock
on a hit. One session per process: the first capture to start owns it,
so the integrated server's joined side does not start another.
Captures write two more kinds, alloc and live (bytes, period 1), into
the same tables as the CPU samples, without the allocator's own frames
at the leaf end.
Measured (fast profile, Linux x86_64, 24 threads, machine at load ~50
from other builds):
- Off: alloc_count_bench 64 B alloc+free pairs, +0.4-0.6 ns a pair
(+9-11% of mimalloc's 5 ns on 1 thread; +1-14% on 8, noisy): one
relaxed load and branch on each side, as designed.
- On, 200 walking bots with a capture: the sampler is 1.2% of the
tick thread's CPU samples (1.24 and 0.77 ms a tick), about 70% of it
.eh_frame unwinding, at ~40-70 us a sample; 63k and 100k samples in
~2 min. MSPT avg on 89/148/224/97 vs off 108/69/217/65 ms: the
overloaded server (TPS 5-14) and the machine load swamp the A/B.
- Capture size: 209 and 231 KB on vs 171 and 171 KB off (+22-35%).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SampleSource gains LIVE (sampled heap bytes still allocated when the
capture stopped, at their allocation's time block), and Overhead gains
heap_samples and heap_sample_ns.
horizon-perf reads them: --select live works in query hot/tree/callers
and in the folded and pprof exports, in bytes (a total, not a rate per
tick); the summary prints a heap line (live and allocated MiB, the
functions holding the most) and the heap sampler's cost.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Interleave the address families of resolved sign-in hosts and bound the
connect time, so a broken IPv6 route no longer fails sign-in with
'Could not reach Microsoft'. Fixes#15013.
Fox.SeekShelterGoal.canUse no longer looks for shade when the fox stands in a
village (ServerLevel.isVillage: within one section of a village POI), so a
village fox keeps strolling in the sun as vanilla's does.
Fox.DefendTrustedTargetGoal is now registered at target priority 3: the fox
takes whoever last hurt a trusted entity as its target, plays FOX_AGGRO at its
feet, sets DEFENDING (the growl and the avoid/stroll guards were dead code) and
drops the flag through Fox.setTarget(null) when the goal stops.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`/profiler stop` prints the saved file and never uploads. Where a captures part is
registered (the official build) the line carries [Open] and [Share] buttons that run
`/profiler open <id>` and `/profiler share <id>` (no id: the last capture saved);
a build without the part answers both "not available in this build". horizon-ctl
gains `profile open FILE` and `profile share FILE`. The profiler harnesses read the
new saved line, and `profile_client_e2e.py --official` checks the buttons and open_at.
Docs: saving, Open and Share are independent of telemetry; Share is the only upload.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The benchmark report format reads its hardware through `horizon-system`, not
`horizon-telemetry`, so `horizon-bench` is an engine crate and the two `-> horizon-bench`
exceptions go. `horizon-perf-format` is engine, `horizon-perf` a tool, `horizon-perf-share`
Horizon. The launcher and `horizon-server` reach the report format and the capture files
through `horizon_platform::{bench, perf_format}`. `horizon-threads` joins the portable
crates, which `horizon-world` already used.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Saving a capture is local. A registered `Hooks::captures` offers the viewer page,
Open (serve the file once on 127.0.0.1 and open the viewer on it) and Share (the
upload). `horizon-perf-share`, a Horizon crate in `horizon-official`'s parts, holds
both, with the addresses in `horizon-addresses` and their env overrides; the upload
code moves there from `horizon-perf`, which gains `horizon-perf open`.
The saved line and the `profile stop` reply carry no address: `open_at` is present
only when a build has the hook. `horizon-client` keeps `horizon-perf` for tests only.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Goat reads HasLeftHorn and HasRightHorn with getBooleanOr(key, true): a
number is true when its byte value is not 0, so an int 256 is a lost
horn. The loader tested the whole number; it now uses flag_or, as every
other boolean there does.
Brings the branch up to date and lays out the new code and tests as
rustfmt wants them, which the verify job checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Review of #14687.
Vanilla reads Attribute.CODEC and Identifier.CODEC with `minecraft:` as the
default namespace and writes them back whole, so `/item replace ... with
iron_pickaxe[attribute_modifiers=[{type:"attack_damage",id:"x",...}]]` saves
`minecraft:attack_damage` / `minecraft:x`. Horizon kept the strings as given:
the wire writer namespaced them, but the host passed the bare name to
`set_modifier_unclamped`, which skips an attribute it cannot find.
- `Items::attribute_modifiers` namespaces the stack's `type` and `id`, so the
host applies what the client shows.
- The saved form (`item_to_nbt`) namespaces them as vanilla does.
- `test_builtin_component_delivery_parity.py`: a `tool` and an
`attribute_modifiers` case (with bare names), run against the 26.3 server.
- Tests: a bare name on the host and across the wire, the saved form, and the
writers' silent-drop path for a tool or modifier they cannot encode.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Shulker reads AttachFace with Direction.LEGACY_ID_CODEC: a byte through
from3DDataValue (abs(data % 6)). The loader took the remainder of the
whole number, so an int 258 was face 0 where vanilla's byte 2 is face 2.
The shulker bullet's Dir already narrowed first.
Every (effect, ambient) pair, known or not, gives the same particle
through the cache as through the plain lookup, and the second pass
over the same pairs makes no registry lookup at all.
Refs #14921
Co-Authored-By: Claude Opus 5.5 <[email protected]>
effect_meta looked up the particle of every visible effect for every
placed guest on every tick: a linear search over the effect table, a
formatted name and a String-keyed registry lookup. The result depends
only on (effect, ambient), so the hub now keeps it per pair. Output is
unchanged. The function moves out of hub.rs into hub/effect_meta.rs,
which keeps hub.rs from growing.
Refs #14921
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Compares the cached equipment with a direct conversion of every equipped
slot over static ticks, after a count change and after a slot is cleared.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Held, offhand and armor stacks were converted to network stacks for every
guest on every tick. Each guest now keeps the converted stack of each
equipment slot together with the ItemStack it came from, and converts
again only when that stack differs by full equality. The /reload path
clears the cache, since conversions depend on the registries.
Co-Authored-By: Claude Haiku 5.5 <[email protected]>
Wolf and Cat read CollarColor with DyeColor.LEGACY_ID_CODEC: a byte
through DyeColor.byId, whose out-of-range answer is the first colour
(OutOfBoundsStrategy.ZERO). The loader masked the id with & 15, so 17b
was orange and -1b black where vanilla's collar is white.
A 0.2 pack lights block terrain and nothing else: mobs, chests, dropped
items and particles keep vanilla's lighting, and no stage lets a pack light
them. This adds the API for that stage, so packs can be written and checked
against it before the game draws it.
- `[entity]` in the manifest and `fn entity(input: EntityInput) -> Surface`:
the texture, the tint apart from vanilla's shade, the hurt flash, the
light, a normal, the position and what is drawn (`kind`). It returns the
material stage's `Surface`, so one function can light both.
- Its resources are the material stage's shadow maps and coloured-light
grid, at the same bindings, so one bind group can serve both. No block
texture helpers, `reflection`, `opaque_*` or `set_surface`.
- The reference gives vanilla's own as the stage's function, read from
`entity_color`: the flash is mixed in vanilla's gamma, before the light.
- `horizon-shader check` takes the stage with the material's limits and
refuses a wrong signature or a constructed `EntityInput`. The game refuses
a pack that has it until it draws the stage.
A pack without `[entity]` is unchanged and sees none of the stage's names:
every other stage's prelude and entries are the same text as before, held by
the names lists. 0.1 refuses the key.
Part of #14583 (step 1) and #12989.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
tools/test_allay_jukebox_source_parity.py runs a vanilla and a Horizon
server and compares every allay data value and level event per phase,
recorded by VanillaProtocolClient's new allay-dance mode. Stacked on
the fix in #13760.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A recording is made on a remote connection, where the process-wide table
may still hold the sea level of a world opened earlier.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
EditBox.onClick selects the word under the pointer on a double click
(EditBox.selectWord: getWordPosition(-1)..getWordPosition(1)) and places the
caret otherwise; MouseHandler calls a click within 250 ms on the same screen a
double click. Horizon's chat input only ever placed the caret.
An alias written over an existing texture changed what every other reader
of that file sees: an unstitched or paletted sprite built from it, another
source's resource, anything that reads the texture as itself. Aliases now
only fill sprite paths that have no texture of their own, which is what
ItemsAdder and Nexo packs rename to; a rename over an existing texture
draws that texture, as before aliases. With that, vanilla's atlases never
list an alias's sprite, so they are no longer read for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AreaEffectCloud extends Entity and never moves, so Entity.saveWithoutId
writes the fall_distance Entity.load read. entity_base_tag, which the
cloud's save uses, took OnGround from the loaded compound but wrote
fall_distance 0.0; it now takes that from the compound too.
A rename whose resource is vanilla's texture copied vanilla's `.mcmeta`
under the sprite even when a pack's filter hid that `.mcmeta`. Vanilla's
`createStackMetadataFinder` stops at a pack that filters the metadata, so
the alias now takes vanilla's `.mcmeta` only where no filter blocks it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
EvokerFangs extends Entity and never moves, so Entity.saveWithoutId
writes the fall_distance Entity.load read. Their save took Motion, the
pitch and OnGround from the loaded compound but wrote fall_distance 0.0;
it now takes that from the compound too.
Entity.saveWithoutId writes fall_distance from fallDistance and
Entity.load reads it back. AbstractArrow.tick never calls Entity.move,
so a trident keeps the loaded value until water resets it. The trident's
save wrote 0.0 and its load ignored the key; it now reads, keeps and
saves it as the arrow does, and water resets it in the ticks that reach
Entity.baseTick. Lava's halving is not modelled for a trident.
The overlay gave every `single` rename's sprite its resource's file, even
when a later source of the atlas (a plain `single` or a `directory` of a
higher pack) listed that sprite from its own texture again. It now resolves
each atlas's whole source list in order with `atlas_sources::list`
(`SpriteSourceList.list`) over the textures the packs and vanilla leave
visible, and gives a sprite its resource's file only when that final
listing is the rename. Both atlases read the same file, so a sprite the
other atlas lists any other way keeps it. An atlas no pack provides is
vanilla's.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A server owner has to know which connection floods or lags, so net rows carry each player's name, client
brand and view distance. server_perf_e2e still checks that the world, seed, MOTD, mob and sign names and the
IP stay out, and now checks that net names both bots.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The window now hands its server connection's byte counters to the capture each frame, so a client capture's
network tab has data. A server-thread capture asked for while the previous one is still being written waits
for that file instead of refusing, which the window had already reported as started.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A capture made with --gpu now writes each timed frame (gpu_frames = 24: its
start on the capture clock, first-to-last timestamp total, every pass and part)
in blocks about once a second, with the CPU ms of every drawn frame. GpuPasses
ms becomes the median over the whole capture instead of the last 15 frames.
Where the device stamps inside passes, every timed frame splits the world
passes into their draw groups (sky, solid terrain, occlusion boxes, entities,
translucent terrain, clouds, weather, hand...) and compute passes into their
dispatches, so GPU culling's parts show without detail mode; Metal keeps pass
boundaries, recorded as GpuDevice.pass_split.
query gpu gains per-pass and per-part samples, p50/p95/p99/max and share of the
GPU frame p50, plus a summary row with GPU and CPU frame percentiles and a
gpu/cpu/mixed bound verdict; query gpu_frames gives the series with slow frames
flagged. Older captures read with empty statistics.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Above 65535, below 1024, not a number and negative all read the invalid-port
reason; 1024, 25565 and 65535 keep the Port Number label, and an empty box
takes the suggestion.
WorldOptionsScreen.setPortError swaps the port box's tooltip from
PORT_INFO_TEXT to INVALID_PORT when tryParsePort rejects the value. Horizon
always showed Port Number, so a red box never said what was wrong.
SulfurCube.canFreeze answers `!hasBodyItem() && super`, so a cube carrying a
block in its body takes no frozen ticks from powder snow and no freeze damage,
while one without a body freezes like the cube mobs do. can_freeze answered
the tag and the skeleton's refusal and nothing else, so a loaded cube shook and
took the 1 a fully frozen mob takes.
The test puts a sulfur cube with a stone body at 0 and at 140 frozen ticks: it
stays at 0 with no damage and melts to 138, where the cube without a body gains
a tick (1) and takes the 1 at 140. Without the arm the loaded cube reads (1,
0.0) like the empty one.
Upload completed chat-started captures on a background worker and show a clickable capture URL. Preserve the local file and an owner-only receipt before pinning, so /profiler share can retry without uploading twice. Use an application user agent accepted by the deployed API. Keep control-socket and environment captures local.
Validated timed and manual in-game completion against the live API with byte-identical download, plus retry/ownership regression, command completion and Clippy. Test uploads removed after validation.
Fixessunrise-org/horizon#267
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Keep bounded raw unit timings during recording and expose exact slow-tail means through meta. Tick rates express work capacity capped at 20 TPS; old or incomplete captures omit lows. Related: sunrise-org/horizon#264.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A folder where the official launcher wrote versions/26.3/26.3.json but not
26.3.jar (a version picked but never played, or a download that stopped)
said "no Minecraft 26.3 client jar", the same as a folder that never had
26.3. It now says the version file is there and to play 26.3 once in that
launcher. A wrong jar and a folder without the version file keep their
messages.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Benchmark A/B runs and --benchmark-set/--profile, the memory section, span
laps through streaming and the world update, the server report and the
/profiler command tree, ported from the original patches onto the files
upstream split out of game.rs.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A `single` atlas source with a `sprite` puts its resource's pixels under
another id (SingleFile.run). ItemsAdder and Nexo server packs name their
models' textures only through these renames ("ia:1199" for
minecraft:block/sea_lantern), and every loader reads a model's sprite
from textures/<sprite>.png, which those packs never ship: their models
drew the missing texture or nothing.
The overlay now gives each renamed sprite of the blocks and items
atlases its resource's file and .mcmeta, as SpriteResourceLoader reads
the resource's metadata. A rename over an existing texture replaces it
in the overlay only. Overlay cache version 9 rebuilds cached overlays.
Part of #14485.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Conflicts kept both sides: the platform's pass timing feature moves into the
device search's new open_adapter, its Options fields and work-attribution
calls follow the code upstream moved out of game.rs, and the perf channel
joins upstream's list of Horizon channels.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Vanilla's bulk range choice and interval select sample every branch over the
whole volume and then pick one value per position, and so did the port. In
the Overworld's final density that is the underground caves (cave layer,
cave cheese, 2D spaghetti) on the two thirds of a chunk's 5x49x5 lattice
above the stone, and four or five scales of each spaghetti noise where one
is read.
The noise samplers can already skip the Ys a caller does not read
(sample_needed, a bit per Y per column), but only the lerp asked. The range
choice now asks its out-of-range branch for the positions whose selector is
out of range, the interval select asks each branch for the positions that
select it, and lerp, unary and binary samplers pass a caller's mask on.
Every sampler is still called, in vanilla's order, and a cache still fills
its whole volume (its buffer is read by later scalar queries), so only noise
that nothing reads is skipped. A test spoils every value a noise was not
asked for and checks the result bit for bit against the choices spelled out
per value.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
The new-or-reused id check and the choice between the two spawn paths move
into `tracks_remote_entity` and `spawn_added_projectile` in
remote_projectiles.rs, so the AddEntity handler calls one function and
remote.rs ends up shorter than on main (docs/layers.md#file-size).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each AddEntity made two passes over `falling` with thousands of blocks in
the air: `spawn_remote_projectile` first removed any entity under the same
id (ClientLevel.addEntity's removeEntity), and `remote_motion` then looked
the new block up from the front of the list, where it is last.
The handler now checks whether the id is already tracked before
`spawn_entity` tracks it. Only then are `falling` and `tnts` filtered for
it; the crystals and the small projectile lists are still filtered every
time, since the client can add to those itself. `remote_motion` and
`move_entity` search `falling` and `tnts` from the back.
8,000 falling blocks, 256 AddEntity packets (debug test build):
10.6 -> 0.9 ms.
Part of #13007
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The server now sends a stack's minecraft:tool and
minecraft:attribute_modifiers (Tool.STREAM_CODEC,
ItemAttributeModifiers.STREAM_CODEC), and the client reads them back as
their codecs save them. Single player's client and LAN guests now predict
mining from the stack's tool and show its own modifiers in the tooltip.
Equipment modifiers (detectEquipmentUpdates, ItemStack.forEachModifier)
come from the stack's ATTRIBUTE_MODIFIERS, not the item type's, so
Player.attack's getAttributeValue(ATTACK_DAMAGE) honours them: +20
attack_damage on an iron pickaxe hits for 21.
Fixes#14641
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Stacked::try_pick with output=None was a destructive check: it ran the
augmenting-path match and deducted matched items from self. Menu::place_recipe
called it as a pre-check before Menu::place, which then tried to pick again on
the already-depleted stacked contents. With exactly enough ingredients for one
craft (e.g. 3 sugar cane for paper), the second pick failed and nothing was
placed — no ghost, no items moved.
Make try_pick non-destructive when output is None (work on a clone). Only the
actual pick (output=Some) mutates self. This also fixes try_pick_all's binary
search, which called try_pick(mid, None) and depleted stacked before finding
the answer.
Clicking a recipe whose ingredients are missing (e.g. paper without sugar
cane) showed the ghost once, but a second click did nothing because
try_place's last_placed guard blocked re-clicking any entry marked
non-craftable. Vanilla always shows the ghost for an uncraftable recipe,
so remove the retry guard. last_placed is still recorded for remote ghost
recipe handling.
Fixes#14616
horizon-client-lib's unit tests spent 71% of their time in the 708 mobs::
tests and 7% in lan::prediction, and nearly all of that was each test
reading the whole prepared root again in a debug build: Assets (block
registry, tags), BlockFacts, MotionResources, MobData, MobTypes, Biomes,
Items and the collision registry, about 7 s a call under load. A test
calling the fixture 16 times paid 16 times (157 s).
The tests only read those tables and build their own World over them, so
mobs/world.rs keeps the vanilla root's bundle (TestResources) in a OnceLock
and run_test_world builds the per-test World on it as before. A mod root
in a test's scratch folder is still loaded fresh each call, since the same
path can hold different content between tests. test_fixtures::vanilla()
does the same for Assets, RenderAssets, collisions and MotionResources;
lan::prediction's steps_in uses it, and motion.rs and entities.rs can
follow.
Same tests, same cases, same assertions; nothing new is #[ignore]d.
cargo test -p horizon-client-lib --lib on an 8-core laptop at 8 threads:
388 s -> 99 s wall (three interleaved runs each), 3061 s -> 770 s summed.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Mob::pushed_by_fluid, the predicate both push sites of
LivingEntity.updateFluidInteraction read: a drowned with its swimming flag set
answers false, the same drowned with the flag cleared answers true, the water
mobs that never take a push (a cod, a squid, a turtle, a nautilus) answer
false either way, and a zombie answers true. Without the drowned arm the
swimming one answers true too.
Drowned.isPushedByFluid answers `!isSwimming()`: a drowned swimming after its
target holds its line in a stream, while one walking the bottom is carried
along like any other mob. Horizon answered the plain LivingEntity rule for
every drowned, so a drowned chasing a player through flowing water was swept
off its path, and the current was applied even while its own move control was
driving it.
pushed_by_fluid now answers the drowned with its swimming flag, which
Drowned.updateSwimming keeps on the mob (`extra.flag`) and the clients read
from the shared flags, and the rest of the table stays as it was.
The General page showed two rows named Preset: Horizon's own preset, which sets the
options below together, and vanilla's options.graphics.preset. Horizon's is now
Horizon Preset (a new en_us entry; other languages fall back to English until
translated), so the two read apart. The group keeps its name. The search finds both
for "preset", and Horizon's alone for "horizon preset"; the shell_choice_step case
looks for the new name.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
LivingEntity.aiStep's freezing, through freeze_tick, in a world with a powder
snow block at the mob's feet: a skeleton, a stray, a bogged, a parched and a
wither skeleton gain no frozen tick (0) where a zombie gains one (1), and one
of them put at 140 frozen ticks melts to 138 with no damage where the zombie
takes its 1. Without the skeleton arm the skeleton freezes like the zombie.
Skeleton.canFreeze answers false, so powder snow builds no frozen ticks on a
skeleton and never deals it the 1 a fully frozen mob takes, and a skeleton
already frozen melts out of it at two ticks a tick. The stray, the bogged, the
parched and the wither skeleton inherit the same false. Horizon only asked the
freeze_immune_entity_types tag — which lists the stray, the polar bear, the
snow golem and the wither — and the worn freeze_immune_wearables, so a plain
skeleton standing in powder snow froze like a zombie.
can_freeze now answers the five skeletons before the tag, which is where
vanilla puts the class override.
The summon and remote-spawn sites build their orb with it, so the new
fall_distance field doesn't grow commands.rs and remote.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity.isInLava is false while firstTick holds, so Entity.baseTick halves
the fall only from the second tick. Review finding on #14440; the
`orb fall distance` group gains an orb summoned inside lava.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #14440: an orb's Entity.fallDistance builds up as it falls and is
saved and loaded (and taken by /data merge). Entity.baseTick's fluid step
resets it in water and halves it in lava; Entity.move's long-move ray
resets it past a fall-resetting block or water, and checkFallDamage adds
the drop and, landing after a fall, posts HIT_GROUND. Adds the
`orb fall distance` command-exec group.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`JukeboxBlockEntity.loadAdditional` -> `JukeboxSongPlayer.setSongWithoutPlaying` brings a song
back only from a saved clock, only under its length, and never starts it (level event 1010 comes
from `play` alone): a record saved without a clock, or with one past its song's end, loads silent
at signal 0. `notifyItemChangedInJukebox` posts `BLOCK_CHANGE` for the block's `has_record` — and
`JukeboxPlayable.tryInsertIntoJukebox` posts a second one with the player as its source, against
the state the click found. `JukeboxBlock` decides both clicks by that state, not by the slot, and
`popOutTheItem` pops nothing from an empty slot.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Pack Options joins the shells pad_clicks_step resets, and its shell arms and
open_pack_options move to options_ui/shell_pages.rs, where main put shell and shell_mut.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Entity ticking waits for every chunk within 2 columns (ChunkMap.prepareEntityTickingChunk), as of #13953. Add Game::load_ticking_neighbourhood for tests and call it from the fixtures of the 51 prepared-data tests that failed since, and correct the item escape test's drag to 0.98F widened to double.
`LecternBlock.getStateForPlacement` sets HAS_BOOK from the item's
`block_entity_data.Book` when the placer may use game-master blocks, so a
lectern placed from a book-carrying item draws its book.
`LecternBlock.getAnalogOutputSignal` reads the state's HAS_BOOK before asking
`LecternBlockEntity.getRedstoneSignal`, so `/setblock ... lectern[has_book=true]`
with no book now reads 14 instead of 0, and `has_book=false` reads 0.
`LecternBlock.resetBookState` sets POWERED and HAS_BOOK in one write, then posts
`minecraft:block_change` (the player who put the book on, none when
`LecternBlockEntity.onBookItemRemove` takes it off) and updates below, so a
sculk sensor hears both.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
SignBlock.useWithoutItem answers PASS when the player may not build and
the sign is not waxed and has no click action to run (SignBlock.java:139-146),
so ServerPlayerGameMode.useItemOn goes on to the item phase
(ServerPlayerGameMode.java:411-427) and ItemStack.useOn's canPlaceOn test
lets a `can_place_on` block place against the sign (ItemStack.java:358-362).
Horizon's sim's sign arm answered SUCCESS, which `use_hand` consumed before
the item was used. `sign_consumes_click` now asks the sign's block entity
the question vanilla asks it, and use_hand only ends the click when it says
a waxed sign, a click-command side or a player who may build takes it.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
WitherBoss.dropCustomDeathLoot spawns the star and then calls
ItemEntity.setExtendedLifetime, which sets age to -6000, so the
age >= 6000 discard takes it at tick 12000 instead of 6000. Horizon
spawned it like any other death drop, at age 0, and lost it after
five minutes.
Fixes#13937.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZzq8MxShENtiMj2DMbm5G
Keep Rust reader artifacts in the game repository, move the viewer source into sunrise-org/horizon-site, and add CLI uploads and remote capture reads. Update fork automation for the organization transfer.
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
S's gamma rounds up in half float, to light just above S that the curve holds at S. Whether that comes back to the same half float depends on the GPU's rounding: on an Apple M4 Pro (Metal) it lands one step lower, so the exact check at S failed there. Every stored value below S still comes back bit-identical.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pack Options (#11486) becomes a screen on the settings shell, as Shaders
is: each Polytone config section is a page, numbers are sliders that stop
at the entry's step, value lists are choices, booleans switch and free
text reads. The first page starts with the pack-wide presets and Reset
all. The shell gives it keyboard and controller focus, Left/Right
stepping, the search (an entry's tooltip included), descriptions with
the performance-impact line in the strip, and Undo from a snapshot taken
when the screen opens. Leaving sends the pack's choices only when they
differ from that snapshot, so an Undo sends nothing.
options_ui's special cases for the page go, with Lines::titled and the
shell list's branch for it. foot_button takes the glyph room itself
rather than through a wrapper. A pad_menu_e2e case, pack_options, runs
the fixture pack through RB, Right, Undo, Left and B.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Vanilla tests every particle's point against the camera frustum moved back
3 blocks (ParticleEngine.extract, QuadParticleGroup, Frustum.pointInFrustum).
Horizon only did it behind the opt-in Particle Culling setting, so by default
ParticleEngine::render built a quad for every live particle.
Frustum::pulled_back moves the camera back along its view direction and
Frustum::contains_point is the point test. ParticleEngine::render_in_view
skips a particle outside that frustum before any sprite, light or quad work;
render_culled applies it too, whatever the setting says. render is unchanged.
game.rs passes the camera's frustum on the default path.
Written from the issue's description of the rule; there is no vanilla oracle.
Fixes#7255
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`PiglinAi` sets `ANGRY_AT` with an expiry when a player breaks a guarded block
near a piglin, and `Brain.Packed` saves every memory with a codec under
`Brain.memories`: the value is the player's UUID and the TTL is the ticks left.
`LivingEntity.readAdditionalSaveData` reads that brain back.
Horizon wrote `minecraft:angry_at` in `mob_to_nbt` but `mob_from_nbt` never read
it: a piglin that a player had angered unloaded with its chunk and came back
with `extra.anger` 0 and no actor UUID, so it stopped attacking and the guarded
event was forgotten.
The load now reads the memory back: a four-int UUID into
`extra.piglin.guarded_angry_at` and the `ttl` into the existing `extra.anger`
counter, with no TTL read as the longest counter there is. The writer moves to
`mobs/piglin.rs` beside it, which also keeps `entity_nbt.rs` from growing.
`tools/port_map/game2-004.tsv` now answers `Piglin.addAdditionalSaveData`:
`CannotHunt` is written from the live flag, and `mob_from_nbt` reads it back.
`mob_from_nbt`/`mob_to_nbt` are the pair `Game::load_mob` and `Game::mob_tree_nbt`
go through. `CannotHunt: 1b` loads `cannot_hunt` true and `can_hunt` false, and
the next save writes 1. `0b` and a missing key load false and save 0.
Without the read the flag stays false and the save writes 0: `left: false`,
`right: true` for the loaded flag.
`Piglin.readAdditionalSaveData` reads `CannotHunt` with
`getBooleanOr("CannotHunt", false)` and `setCannotHunt`, and
`addAdditionalSaveData` always writes it. `AbstractPiglin.canHunt` is false
while the flag is set, and `PiglinAi` asks it before starting a hoglin hunt.
Horizon wrote a hardcoded `CannotHunt: 0` and never read the field, so a
piglin saved with the flag set hunted hoglins again after its chunk reloaded.
The flag now lives in `piglin::Memories`, the save writes it, the load reads
it, and the hunt goal checks `can_hunt`.
Grouping the packets by guest and encoding them on several threads measured
slower than sending them in turn (400 players on 4 idle cores: 39 vs 15 ms a
tick for the encoding), and Hub::guest now finds each guest by binary search,
so the send loop stays as it was.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
AtlasGlyphProvider looks the sprite up in the atlas the component names;
the GUI atlas keeps its sprites under gui/sprites/, so boss_bar/*, widget/*,
server_list/* and icon/* drew nothing. Draw the missing sprite when absent.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The two redirect tests built a fresh wind charge, which this PR now (as
WindCharge.deflect) refuses for its first five ticks; their fixture clears
no_deflect_ticks, and a new test checks a fresh one keeps its course and owner.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Since #13953 an entity ticks only once every chunk within 2 columns is
loaded (ChunkMap.prepareEntityTickingChunk). Horizon has that
neighbourhood ready a few ticks after vanilla, so these groups, which
waited for the forced chunk alone, read vehicles and mobs that hadn't
moved yet.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HolderSetCodec reads an all_of or any_of terms as a list of holders, one
holder alone (at .terms[0]) or a #tag. The dedicated server's startup check
now follows a lone holder like the list's first element, and refuses a world
whose terms names a predicate tag no pack defines (Unbound tags in registry).
The evaluation reads a lone holder too, so a lone any_of term is true when
its predicate is.
Part of #12711
The row said a block entity saved in the wrong chunk keeps its saved
x/z; `chunk::block_entity_pos` now moves it into the chunk that saved it,
so the row cites it.
`chunk::block_entity_pos`'s test pins the arithmetic: (37, 70, -3) in
chunk (0, 0) is (5, 70, 13) after vanilla's `sectionRelative`, an
already-correct position is untouched, and another chunk moves its own
corner.
`a_block_entity_saved_under_another_chunks_coordinates_moves_here`
decodes a chunk whose `block_entities` list names x=37, z=-3 at chunk
(0, 0) and expects the chest at (5, 70, 13). Without the fix it stays at
(37, 70, -3).
`BlockEntity.getPosFromTag` reads the tag's `x`, `y`, `z`, and when they
name another chunk it warns and keeps only the low four bits of `x` and
`z`, so the entity stays in the chunk whose save wrote it. Horizon's
`decode_block_entity` returned the saved coordinates as they were, so a
block entity saved under the wrong chunk's coordinates landed at a
position that chunk may not even hold, instead of where vanilla puts it.
`chunk::block_entity_pos` does the clamp, and the chunk decoder uses it
for every block entity it reads. No line growth in `save.rs` (the size
gate's 3000-line rule).
The eight component branches (#14131-#14138) each edited the same spots and
three of them left an unclosed block in finish_consuming. Merged here:
- item_sync::stack_consumable builds the component a use reads once: the
stack's own when it has one, with Consumable.CODEC's defaults for the
fields it leaves out (1.6 s, eat, entity.generic.eat, particles), else the
item's. finish_consuming, the per-tick particles and sounds and the
instant-consume check use it.
- Consumable.canConsume's food check asks the stack's food component.
- A direct sound reads SoundEvent.DIRECT_CODEC's `sound_id`, and every
ItemUseAnimation name maps (trident, crossbow and spyglass were eat).
- The glider test's doc comment is back on its test.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A splash or lingering potion or experience bottle that a player throws
or a dispenser shoots went into a side list that is not part of the
level, so @e, /data get entity, /execute if entity and /kill never found
it. Vanilla adds it with level.addFreshEntity like any entity.
Throws and dispensers now make the same level projectile /summon does
(ProjectileKind::Potion / ExperienceBottle); the player's throw uses
Projectile.shoot_from_rotation (#13201).
A pure move: game/tweaks.rs becomes game/extras/mouse_tweaks.rs, and
the sort buttons and key leave it for game/extras/sorting.rs. Paths,
visibility (pub(in crate::game)) and imports only; game.rs loses its
mod line.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's 3b34446f84 added the same helper with the fuller ItemStack.isDamageableItem rule (an
explicit DAMAGE component on a non-damageable prototype); this branch keeps that one and its own
ItemDamageFunction.apply.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main's health/equipment.rs (3cdf31772f) runs a hurt in stages and copies the block results
between them; it now copies blocking_item_used too, so a staged hit reports the shield use as
the one-shot hurt does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With a crowd, the tracker's send pass (each active entity's changes and new
pairings, to each viewer) ran on one thread. Each entity's packets are now
made once on several threads, then each viewer's are handed over in entity
order on several threads. Every viewer gets the same packets in the same
order; a test compares both paths byte by byte.
Ideas from HorizonsX (MIT, by cantoptp-bit), commits 091bb8e1c6 and
34e43cef96; code written fresh against main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
main moved more of game.rs out (#13877, #13878, #13880...): the conflict is only the
neighbours of the moved functions; game.rs takes main and drops the four functions
this branch moves to game/render_settings.rs, unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
In a tick where nobody hid, showed or changed team, a move changes every
receiver's connection to the player who moved and that player's own row, and
a row reads only itself, its receiver's view and the tick's players. So each
receiver's row meets the moves in the loop's order on its own, on several
threads, and its packets come out exactly as the one loop sends them. A test
runs both paths over random crowds and compares the bytes per connection.
Ideas from HorizonsX (MIT, by cantoptp-bit), commits 34e43cef96 and
db90336e36; code written fresh against main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A crowd's locator bar visited every receiver's row with a linear search and
then found each packet's guest with a scan of every guest. Rows keep vanilla's
order with an index by source; Hub::guest binary-searches (guests join in
connection order) before scanning; the locator bar's packets are grouped by
guest and encoded on several threads, each guest's in order.
Ideas from HorizonsX (MIT, by cantoptp-bit), commit a50b3439f6; code written
fresh against main.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
WindCharge.deflect refuses while noDeflectTicks (5, one less after each tick,
not saved) lasts: a projectile hitting a young wind charge doesn't redirect it
(Projectile.onRedirectProjectile), and a player's blow goes on as an attack
that hurts nothing. A breeze's wind charge has no such wait.
Harness: tools/test_wind_charge_no_deflect_ticks_parity.py.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
creatures.rs is over 3000 lines (docs/layers.md#file-size). The arrow's hit on the player goes
through shield.rs's arrow_hits_player, which sets combat.piercing_hit; hurt_player_from takes it
as it takes rocket_hit and hands it to player_blocking. Same behaviour as hurt_player_with_pierce.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sentence key is alive when any crate names it: the layer split is moving
client code into crates of its own, and the strings move with it. The
horizon. key check keeps the four client crates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The verify job compared big files against this branch's old base, so main's own growth of options_ui.rs since then failed the size check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ResourceMetadata.fromJsonStream reads one JSON value with Gson and never looks
past it, so packs leave notes after the closing brace. CozyUI+'s highlighted
button ends its .mcmeta with `//` lines: Horizon's strict serde_json read
rejected the file, so the sprite lost its nine slice and animation and drew
as its whole 16-frame strip (#3046).
The nine-slice, GUI animation, texture-flag and atlas sprite readers now share
packs::first_json with pack.mcmeta's reader.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A Silent:1b warden played its emerge, dig, roar, sniff, tendril, attack and sonic boom sounds:
both warden sound helpers now check `silent`, as Entity.playSound does (#8835).
- ForceUnmount was stubbed out: a warden put on a vehicle with /ride or a summon's Passengers
never got off, so it could never dig. It now runs vanilla's unRide (passengers ejected, the warden
set down where its vehicle lets it off) and stops in the tick it starts (#8833).
- WitherBoss.customServerAiStep counts the side heads' idle updates only on Normal and Hard: on Easy
a wither fired its random blue skulls all the same (#8840).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Tadpole.feed draws its happy_villager particle on the client too; a tadpole
is never isBaby, so the client's feed prediction skipped it (#8554).
- Panda.mobInteract's setInLove broadcasts entity event 18, the love hearts
(#8555).
- DolphinSwimToTreasureGoal.start broadcasts entity event 38 once it finds a
treasure, and Dolphin.mobInteract answers a #minecraft:fishes item with
SUCCESS on the client, so the hand swings and the fish isn't eaten (#8567).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
scrub.rs moves unchanged from horizon-telemetry to horizon-system (engine);
horizon-telemetry re-exports it at horizon_telemetry::scrub for the launcher.
The repro export (agent control) reads it there, so gameplay no longer uses
horizon-telemetry for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
rss_bytes, cpu_seconds and load_average move to horizon-system (engine);
horizon-bench re-exports them for the launcher. horizon-system's cpu() and
ram_bytes() become public and replace horizon-bench's identical copies.
The dedicated server's performance report and cem_expr_bench read them
there, so gameplay no longer uses horizon-bench for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Part of #11857. new_renderer, adopt_renderer, forget_atlas_slots and all_sections_changed join game/render_settings.rs. Methods that were private in game.rs become
pub(super), the same reach.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A sentence key is the English text itself, so rewording a setting's text left
the old sentence, and its translations, in all 87 lang files. Eight of them
are no longer named anywhere in the code: the five old descriptions #13802
lists (FOV, HDR output, view bobbing, the two entity culling modes) and three
nits labels. They are removed from every file.
every_sentence_key_in_en_us_is_one_the_code_names fails on a sentence key
that no client crate names as a string literal, so the next rewording can't
leave one behind. The source walk it shares with
every_horizon_key_the_code_names_is_in_en_us now also reads
horizon-client-input, which asks for the gamepad's "Hotbar: Next Slot".
bundled_horizon_strings_follow_the_language checked ru_ru through
Language::load, whose en_us layer fills every gap, so it could not fail. It
now reads ru_ru.json's own keys.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ClientPacketListener.handleSetCamera takes any entity the level holds, but
remote_set_camera only looked among the mobs, so a camera on an armor stand
(the usual target of a server's cutscene or menu scene) was dropped: the
view stayed on the player, who kept moving and turning. The stand now holds
the camera like a mob does: the view looks out of its eyes along its yaw,
the player sends no moves, the crosshair skips the stand and first person
does not draw it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ServerLevel.tickThunder picks its strike column via Level.getBlockRandomPos(minX, 0, minZ, 15), which advances Level.randValue (Ticks::block_random_pos) and extracts bits 2..=5 for X and 10..=13 for Z instead of drawing from Level.random.
SkeletonTrapGoal.tick draws each additional trap horse's horizontal push (two triangle(0.0, 1.1485) draws) from the original trap horse's own random after creating and mounting its skeleton rider, rather than drawing from Level.random before rider creation.
Roll pending loot before automation accesses chest-boat slots or destroying
removals spill them. Keep occupied slots, synchronize open-menu copies, and
award container loot only for the player opening a pending table.
Co-Authored-By: Codex (GPT-6) <[email protected]>
`entity_nbt.rs` is over the 3000-line limit and may not grow (docs/layers.md
#file-size), so the goat's save pair moves into `mobs/goat.rs`: the screaming
and horn fields `Goat.addAdditionalSaveData` writes plus the long jump
cooldown memory, and their read side. `entity_nbt` keeps a one-line call.
`Goat.addAdditionalSaveData`'s row in `tools/port_map/game-012.tsv` names the
three own fields; it now also names the `GoatAi` `LONG_JUMP_COOLDOWN_TICKS`
brain memory that the save covers.
`mob_to_nbt`/`mob_from_nbt` are the pair `Game::mob_tree_nbt` and
`Game::load_mob` go through. A goat with 57 ticks of long-jump cooldown saves
`Brain.memories."minecraft:long_jump_cooling_down"` as `{value: 57}` and the
reloaded goat's `frog::jump_cooldown` is 57 again. A goat with no cooldown
writes no memory, even over a compound that still names one.
Without the write the save has no memory (left `None`, right `Some(Compound({"value": Int(57)}))`), and the
reloaded goat has no cooldown (left `None`, right `Some(57)`).
`GoatAi` sets `LONG_JUMP_COOLDOWN_TICKS` when a goat long jumps, and
`CountDownCooldownTicks` counts it down; `Brain.Packed` saves every brain
memory with `LivingEntity.addAdditionalSaveData` and reads it back.
Horizon kept the cooldown in the goat's `extra.memories` (the shared
`frog::jump_cooldown` the long jump goal asks) and wrote no `Brain`, so a goat
that had just long jumped unloaded with its chunk and came back ready to jump
again. The save now writes `minecraft:long_jump_cooling_down` as the integer
memory it is and the load puts it back in the goat's memories.
The design note and graphics.md said a display at 1000 nits or more shows
the scene as written. That holds at a paper white of 203; in general the
curve is the identity where Peak Brightness is at least 1000/203 times
Paper White, so say that.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Paper White now runs from 10 to 480 nits and Peak up to 10 000 (#12881): the tone curve
test checks that the world shows exactly as written wherever the display's peak reaches
the scene's, at 480/10 000 and 10/200 nits too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the float frame the world's light no longer depends on the display. Light sources
multiply their brightest pixels by fixed gains in multiples of paper white (1.6, 2.4,
2.8 and 4 from faint glowers to the sun), and HDR lighting's strongest light by 3. The
tone curve alone fits that scene to the display: light above the scene's peak (1000/203
of paper white, a 1000-nit display at BT.2408's 203-nit white) is held at it, keeping
its hue, then BT.2390's EETF maps the scene's peak onto the display's.
For players: on a 1000-nit display the world shows exactly as written; a 1500-nit or
brighter one shows the same picture, the sun included, instead of a brighter sun and
lava; a dimmer one keeps its midtones and rolls the highlights off below its peak.
Before, the gains scaled with the display's headroom and the curve then compressed
that light again from a peak it derived from the same headroom, so the look changed
with the Peak setting.
Light Horizon doesn't lift (HDR Lighting off, the title, a 0.2 pack's own) has no
known peak, so there the curve keeps the source that follows the display, as the 0.2
contract describes. HDR off, HDR Lighting off, the HUD, the black lift, the SDR
screenshot's shoulder and the 16-bit or 8-bit compatibility frame (HDR's default until
step 5, which changes the look once) are unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Summary tiles read the wasm's own series stats (new optional `stats` on series rows, asked for with filters.stats); the old browser-side statistics disagreed with the CLI summary (MSPT 8.9 against 10.7, CPU 8.47 against 6.6).
- Queries no longer carry the real file name, so the Query tab never shows it.
- Slow ticks: every kept tick opens into its full span tree; the meaningless min/median/p95 of the slowest 16, the histogram and the sparkline are gone. The summary no longer repeats the worst ticks and lists only the top steps of the tick.
- Metadata: tabs that only repeated Memory, Network and World are gone, rows without data are left out, empty tabs are hidden, enum values read as words, and the mod crate column reads the `crate` field the wasm sends.
- Units on every number, frame figures say frames, warnings are worded from their codes, compare rows have readable names and drop an all-"not enough data" verdict column, network packet columns hide when nothing was counted, TPS windows longer than the capture are not shown.
- Keyboard focus is no longer hidden under the sticky tab bar, and rows that open show a chevron.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The diff gains opt-in function and call-path rows (--calls, calls in hp_diff), matched by stable id. One comparison state feeds Summary, Spans, Functions and the Call tree; rows only the second capture has are listed apart.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Spans and functions the profiler meets most (tick_blocks, tick_lan, streaming,
tick_mobs, lan_chunks, random_ticks, allocator and memory calls...) get a short
explanation keyed by stable id in the translation file, shown as an accessible
hover and focus tooltip. Every row of the Summary, Spans, Functions, Call tree
and Slow ticks views copies its stable id from a control that appears on row
hover or keyboard focus, so rows stay clean at rest.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A List / Flame graph toggle in the Call tree tab draws the current side, thread,
selection, grouping, merge mode and focus as a canvas flame graph: widths are time,
colours are the tree's crate groups, hover shows name, time and share, a click zooms
in with a trail back, and arrow keys, Enter and Escape do the same from the keyboard.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The tree rows carry their crate, and a request can ask for each row's own time as a percent.
The tree tab is split into focused components; chains of single-child functions share a line.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The no-capture screen is now a landing page: what the viewer reads and how to get a capture. A tab appears only when its query answers with rows for the side on show, so GPU is gone from a server and empty tabs are gone everywhere; Metadata's sub-tabs follow the same rule.
Health, and Summary's network and dimension cards, repeated other views and are gone. So are the file name and size, the report box, New profile, the sort direction and row limit, the placeholder flame graph, and id chips.
Fixes: slow ticks say they cover only the slowest kept, in time order; query errors show their real message; Compare is a table of real rows for the side on show; table headers line up with their columns.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Removes sharing entirely: the Share button, share links, the upload API, its S3 storage and the CLI's `share` and `upload`. A capture stays on the machine.
Fixes:
- ureq and the snapshot files are gone, so the wasm builds and the tree is 7,600 lines lighter
- merge mode hides wrappers and merges a function across its callers
- `group --by mod` backs the mods view, charging a sample to its innermost mod frame
- a failed compare shows its error, and the compared capture is closed when replaced
- Hot shows milliseconds rather than calling them percent; Tree bars share one scale
- tick details line up; wasm replies are typed structs; launcher lock is current
Polish follows docs/design-system.md: Geist, list rows with aligned number columns, one accent, one sticky tab row, a real opening screen, focus states and reduced motion. Panels share one query hook, and dead icons, strings and exports are removed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fan out implementation of the remaining perf-platform web UI pieces and upload API:
health/metadata, call tree/group-by/select/detail/search, flat/mods/merge/info,
flame graph and ticks tab, compare/GPU/export/copy/share/open links, and
upload route handlers, S3 storage, validation, share pin, sweep, rate limit.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Upstream moved Game::shut_down and idle to game/window_handler.rs and loading_frame and start_game to game/loading_window.rs; finish_profile and the benchmark set and profile fields move with them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The slow ring no longer shows up as span lists in the log, but whose packets or events took a tick
stay in the server owner's log as a separate line. pacing_bench keeps only frames over the threshold
recorded during the capture.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The slow ring and the captures keep the steps of slow ticks and frames; the notes channel that only fed the log line (event timings, packet notes, spawn counters) goes, and the spawn counters move to the counter registry.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The server sends its finished file in 32 KiB chunks over horizon:perf, at most 256 KiB/s and not while the guest has a backlog. The client merges it field by field into its own stream (PartMerger, no decode), or saves it unchanged for profile server alone; a cut transfer ends the file with SERVER_PART_INCOMPLETE.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A client asks a Horizon server (LAN host, dedicated server) to capture its side: operator level 4, one capture at a time, the server logs who profiled and keeps its copy in reports/. profile server start|stop work on a joined server; a bare profile also asks and a refusal is a warning in the client file.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A server-only mark replied capture-relative and a joined one on the game clock; both now use the game clock.
The e2e checks the server mark against the client mark's game-clock t.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The integrated server thread writes its side (kind INTEGRATED) into the window's capture file, on the same clock, and a bare profile means both. profile server writes the server thread's own file.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds BenchResult (field 21) and Attachment (field 22) to the capture contract. horizon-perf reads bare benchmarks/*.json as one-sided captures, answers query bench and prints bench lines in the summary, and attach appends an attachment on the wire bytes so unknown fields survive. The game's --benchmark --profile records a capture and embeds the report through profile stop --bench; server_bench.py --profile records Horizon's measure window with horizon-report and attaches the run record.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The Chrome-trace writer in spans.rs is replaced by per-thread buffers drained once a second into
TimelineChunk (field 15), capped at 60 s or 256 MiB with timeline_stopped_t and a warning.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Adds the gpu field (GpuPasses per timer) to the contract, written from the
renderer's PassTiming readings at stop; profile start --gpu[=detail], the
HORIZON_SHADER_PACK_TIMING and HORIZON_GPU_PASS_DETAIL aliases, horizon-perf
query gpu with fragments_per_pixel, a summary line and a meta count.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
One-line docstring and debug-key comment, F3+O feedback decided by who pressed stop, hardware described off the render thread, and the gauge probe spawned on the first frame with this second's counts.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The window's frames feed an always-on client health history and slow ring.
profile start|stop|status|mark (and F3+O, HORIZON_PROFILE=DIR) write a .hzperf
into reports/ in the game home, with the render thread's samples, retention and
the translated saved message. The capture session is shared with the dedicated
server's through a Role; the gauge probes run off the frame thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A Recording now turns spans and job stats off when it is dropped, so a capture that failed
to start or a shutdown no longer leaves them on. A leaver's unread bytes are lost (noted on
TrafficRecorder). The e2e script's docstring is one line and its build command is in
docs/perf-platform.md, which also says the privacy checks' split and the dropped between-tick spans.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The dedicated server's capture now carries span_blocks (5 s blocks with parents), the spans
summary (totals, histogram, 16 worst ticks with their trees), workers, per-player net bytes by
join order and the world at start and stop with sampled mob costs. perf-*.json and .txt are no
longer written. capture.proto gains the game_rules and horizon_options allowlists and the mob
timing overhead fields. Spans recorded between ticks no longer count in a tick.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Reads the samples section: functions merged by normalized name, ids over the
normalized name and call path, filters (thread, select, slow, where, window),
several captures for frames, meta counts samples, summary gains hot functions
and the entity/block/chunk splits.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The samples field (SampleProfile) is built from horizon_threads::Profile in
horizon-client-render: a deduplicated function table, a call-node chain per
stack, a context table and weights per thread, state, slow flag and time block.
The sampler's aggregate gains time blocks. horizon-report samples the tick
thread on Linux unless --no-samples, and finishes the file off the tick thread.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The always-on server health history and slow ring feed a capture file
(Start, history, slow_history, health, counters, slow, Stop) in reports/,
with retention of 30 files or 1 GiB that never touches renamed files.
horizon-report accepts --no-samples and --profile; horizon-server report
prints the saved path and the web UI address.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Replies are serde structs (one Envelope<R>, a row struct per op, typed meta, watch events, diff and bench diff) and
closed vocabularies are enums (Op, Select, ContextKey, GroupBy, Unit, Verdict, HealthField, Flag, the proto name enums).
The CLI parses strings into them at the edge; no serde_json::Value, json! or string-keyed match is left in the library.
The JSON is the same as before as parsed JSON (tests/golden holds 49 cases captured from the old code). Intentional
differences, each visible in the golden diff:
- --where dimension=X is now the key dim, so dim and dimension merge in selection.where
- follow --json spells a stop reason window_cap (it said "window cap"), like query meta
- an unknown --by is a usage error at the edge instead of a world-only query error
- JSON object keys now come out in declaration order, not alphabetical
horizon-threads gets a feature alloc-count (alloc-census implies it) with
CountingAllocator: each thread owns a cache-line slot of four counters, so a
block costs two plain load/store bumps. GameAllocator picks the census, the
counting mimalloc or plain mimalloc, and main.rs and the scene_replay example
declare it; without the feature the allocator is unchanged. The census counts
the same way.
In a counting build stats, bench and fly reply with alloc (window-thread
allocations and KiB per frame, all-thread rates, net growth), and stats raw and
fly raw add alloc_count and alloc_bytes beside frame_ms. Other builds reply
null. docs/memory.md gets the section with the measured cost.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Describes only what exists: horizon-ctl memory and what each number means, the
alloc-census build (how tags are chosen, what it costs: +1.8% resident, frame
CPU time within the run-to-run spread, measured with --benchmark standard),
spans/trace/stats, resident memory in benchmark reports, scene_replay --memory
and the system-allocator feature for heaptrack on small probes.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
symbols.rs resolves addresses against this process's own binaries (DWARF through addr2line, the ELF symbol table for full paths), keeping base file names only. tree.rs names the aggregate's trie, merges it into a function tree and ranks functions by own and total samples. The pprof, folded and HTML writers stay out; the capture's samples field replaces them. Also fixes the spans test's work:: path and a clippy as_chunks lint.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`profile::start` runs a writer thread that drains the sampler's rings every 100 ms, unwinds the stack copies there and folds each record
into a call-stack trie (131k nodes at most) plus counters per stack and context (span, entity type, chunk, block, dimension, slow tick).
A tick marker decides afterwards whether the samples of that tick count as slow, so the report can keep only the ticks over a threshold.
Tick durations go to a fixed log-bucket histogram per thread with the ten worst ticks. `Session::status` reads progress while it runs.
`Config::wall` arms a second timer per thread on the monotonic clock, signalling SIGRTMIN so it queues instead of merging with the
CPU-time SIGPROF; each handler blocks the other, so they never nest on one ring. A wall sample is marked running when the thread's CPU
clock advanced by more than half the period since its previous wall sample (the handlers' own microseconds don't count).
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The innermost span (spans::guard), the entity type before each mob, item,
cart, projectile and block entity tick, the chunk and block of random and
scheduled block ticks, the chunk in natural spawning, the level's dimension,
and a tick marker with its duration at the end of each server tick and each
client frame. Plain stores into the sampler's thread-local; the new
horizon_threads::profile module makes them no-ops where the sampler does not
run and interns the names behind the ids.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Frame pointers cost 29-34% world load in release builds, so the release build keeps them off and the SIGPROF handler now copies
the interrupted registers and the top 16 KiB of the thread's stack (clamped to its bounds) into a byte ring, memcpy only.
`Session::drain`, on the caller's thread, unwinds each copy with the loaded objects' `.eh_frame` tables. The frame-pointer walk
stays in the handler as the fast path when this build keeps frame pointers (`Unwind::Auto`, decided at start from a probe).
Ring entries are variable length; a ring holds two seconds of samples by default, and a full one drops and counts as before.
New dependencies (Linux only): `framehop` 0.16 (MIT/Apache-2.0, samply's unwinder; default features off, so no Mach-O/PE
parsers) unwinds from `.eh_frame`, and `object` 0.39 locates the sections in each loaded ELF. There is no in-tree unwinder, and
writing a CFI interpreter is the alternative. Both share one `gimli` 0.33, which was not in the lock before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`unwind` walks frame pointers within a thread's stack bounds (shared with the allocation profiler). `sampler` arms a per-thread
CPU-time POSIX timer that signals SIGPROF to its own thread; the async-signal-safe handler copies the thread's context and
stack into a preallocated ring, and `tick_end` writes tick-duration markers so a report can keep only slow ticks.
Nothing is installed or armed until `sampler::start`. Frame pointers stay off in `.cargo/config.toml`: the benchmark
shows world load +29% (7 of 8 pairs) with `-C force-frame-pointers=yes`.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
step moves above tables' doc comment. The docs now say GPU culling uploads is a pass entry of its
own, only in detail frames on devices with encoder timestamps, not a part of GPU culling. A new
GPU test times both phases and the uploads and checks detail leaves the counts and draws as an
untimed frame's; the test device now asks for SHADER_EARLY_DEPTH_TEST so phase 2 can run.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`horizon-ctl gpu-passes detail` turns on dispatch times and, on Vulkan, per-pass counters;
stats and bench add `gpu_pass_detail`, `gpu_pass_parts` and `gpu_counters`, F3 lists the parts
and the six passes with the most fragments, and `scene_replay --gpu-detail on` reports both per
view after the timed frames. RADV counts fragments per 2x2 quad, so a calibration draw at setup
finds each device's `fragment_unit` and `fragments_per_pixel` is computed from it.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`gpu-passes detail` frames now alternate between a parts frame (times) and a counters frame
(vertex, clipper and fragment shader invocations per world pass, split into segments such as
before translucent, occlusion boxes and translucent terrain). A counters frame throws its times
away, and CountedPass ends its query on drop, so none stays open at the end of a pass. Vulkan
only; a device without pipeline statistics keeps parts frames.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`gpu-passes detail` (the renderer's `set_pass_detail`, or HORIZON_GPU_PASS_DETAIL) times each
dispatch of GPU culling (apply, cull, scan, scatter; count2, scan2, scatter2) and its uploads
plus clear_buffer, so a slow pass can be split. A timestamp pair brackets each part inside one
compute pass where the device has TIMESTAMP_QUERY_INSIDE_PASSES, else each part gets a pass of
its own. Off by default; `on` records the same queries and passes as before.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
land_frame told the GPU timer and a shader pack's pass timer that their frame
was submitted, but not Horizon's own pass timer: with the submit thread on, its
first timed frame was read and every later one waited forever, so gpu-passes,
F3 and scene_replay showed the first frame's pass times only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
horizon-client --benchmark travel flies 512 blocks at 16 blocks/s with the time
and weather frozen, then holds at the end for four blocks: the renderer after
travel, where GPU culling's slowdown shows. Scenario::parse replaces scenes();
--benchmark-set SWITCH=VALUE sets one of the SWITCHES (defaults as gfx has them)
before the world opens, and a refused switch ends the run with the game's
reason. The bench control word takes raw like stats and fly, and the report gets
gpu.<phase>.p50_ms and cpu.<phase>.p50_ms for the trip and hold phases.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
horizon_bench::compare pairs the ab.a/ab.b blocks (B A A B order) and gives the
Hodges-Lehmann effect, the exact Wilcoxon signed-rank interval, a verdict, the
blocks' noise and the first-block flag. tools/bench_stats.py computes the same
numbers in Python and checks a report's stored result against its frames.
Report gains an optional ab field; the format stays version 1.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The new crate holds piece F1 of docs/perf-platform.md: capture.proto (Capture's
top level with Start, Stop, strings, marks, warnings, health, history, the slow
ring and counters), compiled by protox + prost-build in build.rs, and the gzip
writer (flate2, rust_backend) that appends whole top-level fields with a sync
flush after each, on a horizon-perf-writer thread fed by a 4,096-field bounded
channel that drops and counts instead of waiting.
Tests: the frozen tests/fixtures/v1.hzperf decodes to its listed values; after
every flush the file on disk inflates to exactly the fields written; a file cut
at random points yields only whole fields, in order; unknown field numbers are
skipped; the descriptor lint passes capture.proto and rejects a missing
_UNSPECIFIED, a JSON-name collision and a group; a full channel drops, counts
and never waits, and Stop records the drops.
Fixes#2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream #11268 configures a placed frame, painting or armor stand through
stack_configured_entity. Keep its placement code, add the item's custom name
there (a painting took none, an armor stand's lost its style), and route
apply_item_stack_config through the same helper.
ArmorStandDispenseItemBehavior applies the same stack config with no user
after the dispenser's yaw. BlockAttachedEntity.readAdditionalSaveData keeps
the Pos block for a block_pos 16 blocks or more away, and a painting with an
unreadable variant still loads the rest of its data.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
HangingEntityItem.useOn and ArmorStandItem.useOn apply
EntityType.createDefaultStackConfig: the item's custom name and custom data,
then minecraft:entity_data merged over the entity. The hanging entity's
survives() runs on the configured entity; the stand turns to the player's
yaw, pitch 0, afterwards. The vehicle helper becomes apply_item_stack_config,
and /data merge entity now loads a painting (facing, block, known variant).
Fixes#11242
Co-Authored-By: Claude Opus 5.5 <[email protected]>
MinecartTNT reads explosion_power and explosion_speed_factor with
getFloatOr (4 and 1), clamped to 0..128, writes each only off its
default, and explode() blows with explosionPowerBase +
explosionSpeedFactor * nextDouble() * 1.5 * min(speed, 5). Horizon blew
every cart at 4 + 1.5 * speed * r and kept whatever the loaded compound
had for the fields, unclamped.
Command harness group "tnt minecart explosion power": 13/18 -> 18/18.
Fixes#11167
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 17:28:56 +03:00
1149 changed files with 84175 additions and 12486 deletions
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
@@ -102,6 +103,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
| `horizon-perf-format` | The `.hzperf` capture contract ([docs/perf-platform.md](docs/perf-platform.md) §3): `proto/horizon/perf/v1/capture.proto`, its generated types, the gzip writer and its thread. No Horizon dependencies; nothing in the game reads a capture |
| `horizon-telemetry`, `horizon-telemetry-server`, `horizon-bench` | Opt-in data collection, its ingest server, the benchmark report format |
| `horizon-system` | The machine the game runs on (OS, CPU, memory, drive, power, GPU maker and memory), for the crash report, the renderer's GPU info and telemetry |
| `horizon-platform` | The platform's public API: what the Horizon layer may use of the engine ([docs/layers.md](docs/layers.md)); re-exports `horizon-system` today |
@@ -213,7 +215,7 @@ state stays separate from the library tests. This compiles gameplay twice for ex
/// `profile` (and the heap session's `heap`) as a `SampleProfile` of `side`; strings go through `strings`, whose new entries the caller appends first.
/// One `GpuPasses` per timer that has timed something, its times the medians of `log` where it has them; the caller appends `strings`' new entries first.
//! Named timings within a frame, for finding what makes a frame slow:
//! `HORIZON_SPIKES=MS` prints the spans of every frame whose CPU time
//! exceeds MS milliseconds, and of every server tick (dedicated or
//! integrated) longer than MS. Each span keeps its wall time and the
//! thread's CPU time inside it, so a span that waited (for a lock, the GPU
//! or a core on a busy machine) shows apart from one that worked. Costs two
//! clock reads per span when enabled and a relaxed load otherwise.
//! Named timings (wall and thread CPU) of a frame or tick, on with `HORIZON_SPIKES` or while a capture records; the slow ones go to the slow ring, not as span lists to the log.
assert!(slept.as_millis()<25,"{slept:?} while asleep");
assert!(worked.as_millis()>=10,"{worked:?} while busy");
}
/// Each thread's finished spans reach the timeline with their depth and nesting in time; threads alive together under one name get labels of their own, ones after another share a lane.
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.":"Growwe verre terrein in plaaslike wêrelde. Geïntegreerde GPU's stop by 128 stukke.",
"Reduced draws far terrain coarser: faster on this integrated GPU.":"Verminder trek veel terrein growwer: vinniger op hierdie geïntegreerde GPU.",
"Reduced draws far terrain coarser, for GPUs it slows down.":"Verminder trek veel terrein growwer, vir GPU's word dit stadiger.",
"Field of view. Wider shows more terrain at once.":"Gesigsveld. Wyer wys meer terrein op een slag.",
"Caps frames per second; a cap saves power and heat.":"Caps rame per sekonde; 'n pet spaar krag en hitte.",
"Waits for the display's refresh: no tearing, a little latency.":"Wag vir die verversing van die skerm: geen skeur nie, 'n bietjie vertraging.",
"Size of menus and the HUD.":"Grootte van spyskaarte en die HUD.",
@@ -339,13 +338,11 @@
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.":"Maak gekartelde rande glad. FXAA: sagter. TAA: gladste, bewegende dinge kan loop.",
"Renders the world smaller and rebuilds it with TAA. Faster, softer.":"Maak die wêreld kleiner en herbou dit met TAA. Vinniger, sagter.",
"Sharpens the upscaled picture. Higher: crisper edges, more shimmer.":"Verskerp die opgeskaalde beeld. Hoër: skerper rande, meer flikkering.",
"HDR output on an HDR display: lava, fire and sunlight shine brighter.":"HDR-uitset op 'n HDR-skerm: lawa, vuur en sonlig skyn helderder.",
"How frames reach the screen while VSync is off.":"Hoe rame die skerm bereik terwyl VSync af is.",
"Efficient sleeps until the next frame; Precise spins, like vanilla.":"Doeltreffende slaap tot die volgende raam; Presiese draaie, soos vanielje.",
"Fills the screen (borderless).":"Vul die skerm (randloos).",
"Brightens dark places.":"Verhelder donker plekke.",
"Lowers the frame rate when minimised, or idle too.":"Verlaag die raamtempo wanneer dit geminimaliseer word, of ook ledig.",
"Sways the view while walking.":"Swaai die uitsig terwyl jy stap.",
"Darkens the screen's edges.":"Maak die skerm se rande donkerder.",
"Shows when the world saves.":"Wys wanneer die wêreld red.",
@@ -556,7 +553,6 @@
"horizon.splitscreen.online_only":"Hierdie bediener is in aanlynmodus: vanlyn profiele kan nie daarby aansluit nie; elke speler het sy eie Minecraft-rekening nodig.",
"horizon.splitscreen.own_account":"Hierdie bediener vereis elke speler se eie Minecraft-rekening",
"horizon.splitscreen.local_only":"Ander spelers kan slegs met hul eie Minecraft-rekening by hierdie bediener aansluit: vanlyn profiele speel in jou wêreld, op LAN of op 'n bediener op jou plaaslike netwerk.",
"Reuses settled boxes; lid movement stays animated.":"Hergebruik afgewikkelde bokse; dekselbeweging bly geanimeerd.",
@@ -621,10 +617,6 @@
"horizon.agent.session_opened":"Geopen vir hierdie sessie deur die program wat die speletjie begin het.",
"horizon.agent.socket":"Sok: %s (kopieer die besonderhede vir sy volledige pad)",
"horizon.agent.local_only":"Slegs plaaslik: agente op hierdie rekenaar, nooit die netwerk nie.",
"%s nits":"%s nete",
"Auto (%s nits)":"Outo (%s nete)",
"%.2f nits":"%.2f nete",
"Hides models behind opaque blocks. Entities update. Off with custom packs.":"Versteek modelle agter ondeursigtige blokke. Entiteite-opdatering. Af met pasgemaakte pakke.",
"Runs the mob AABB precheck after this many loaded mobs. Zero disables it.":"Voer die skare-AABB-voorafkontrole uit ná hierdie aantal gelaaide skare. Nul skakel dit af.",
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.":"التضاريس البعيدة الخشنة في العوالم المحلية. تتوقف وحدات معالجة الرسومات المدمجة عند 128 قطعة.",
"Reduced draws far terrain coarser: faster on this integrated GPU.":"تم تقليل الرسم على التضاريس البعيدة: بشكل أسرع على وحدة معالجة الرسومات المدمجة هذه.",
"Reduced draws far terrain coarser, for GPUs it slows down.":"يتم تقليل الرسوم على التضاريس البعيدة بشكل أكثر خشونة، أما بالنسبة لوحدات معالجة الرسومات فإنها تبطئ.",
"Field of view. Wider shows more terrain at once.":"مجال الرؤية. يُظهر العرض الأوسع المزيد من التضاريس في وقت واحد.",
"Caps frames per second; a cap saves power and heat.":"قبعات الإطارات في الثانية الواحدة؛ غطاء يوفر الطاقة والحرارة.",
"Waits for the display's refresh: no tearing, a little latency.":"في انتظار تحديث الشاشة: بدون تمزق، وقليل من زمن الوصول.",
"Size of menus and the HUD.":"حجم القوائم وHUD.",
@@ -339,13 +338,11 @@
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.":"ينعم الحواف الخشنة. FXAA: أكثر ليونة. TAA: قد تتبع الأشياء الأكثر سلاسة وتحريكًا.",
"Renders the world smaller and rebuilds it with TAA. Faster, softer.":"يجعل العالم أصغر حجمًا ويعيد بنائه باستخدام TAA. أسرع وأكثر ليونة.",
"Sharpens the upscaled picture. Higher: crisper edges, more shimmer.":"يزيد حدة الصورة المرتقاة. أعلى: حواف أوضح ووميض أكثر.",
"HDR output on an HDR display: lava, fire and sunlight shine brighter.":"إخراج HDR على شاشة HDR: تتألق الحمم البركانية والنار وأشعة الشمس بشكل أكثر سطوعًا.",
"How frames reach the screen while VSync is off.":"كيفية وصول الإطارات إلى الشاشة أثناء إيقاف تشغيل VSync.",
"Efficient sleeps until the next frame; Precise spins, like vanilla.":"ينام بكفاءة حتى الإطار التالي؛ يدور بدقة، مثل الفانيليا.",
"Fills the screen (borderless).":"يملأ الشاشة (بلا حدود).",
"Brightens dark places.":"يضيء الأماكن الداكنة.",
"Lowers the frame rate when minimised, or idle too.":"يخفض معدل الإطارات عند تصغيره أو في وضع الخمول أيضًا.",
"Sways the view while walking.":"يتأرجح المنظر أثناء المشي.",
"Darkens the screen's edges.":"لتغميق حواف الشاشة.",
"Shows when the world saves.":"يظهر عندما ينقذ العالم.",
@@ -556,7 +553,6 @@
"horizon.splitscreen.online_only":"هذا الخادم في وضع الاتصال: لا يمكن للملفات الشخصية غير المتصلة الانضمام إليه، وكل لاعب يحتاج حساب Minecraft خاصًا به.",
"horizon.splitscreen.own_account":"يحتاج هذا الخادم حساب Minecraft خاصًا بكل لاعب",
"horizon.splitscreen.local_only":"يمكن للاعبين الآخرين الانضمام إلى هذا الخادم فقط بحساب Minecraft خاص بهم: أما الملفات الشخصية غير المتصلة فتلعب في عالمك، على LAN أو على خادم في شبكتك المحلية.",
"Hides models behind opaque blocks. Async checks off-thread; entities tick.":"يخفي النماذج خلف الكتل المعتمة. غير متزامن يتحقق من خارج الموضوع؛ علامة الكيانات.",
"Block Entities":"حظر الكيانات",
"Shulker Boxes":"صناديق شوكر",
"Reuses settled boxes; lid movement stays animated.":"إعادة استخدام الصناديق المستقرة؛ تبقى حركة الغطاء متحركة.",
@@ -621,10 +617,6 @@
"horizon.agent.session_opened":"تم فتحه لهذه الجلسة بواسطة البرنامج الذي بدأ اللعبة.",
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.":"Yerli dünyalarda kobud uzaq ərazi. İnteqrasiya edilmiş GPU-lar 128 hissədə dayanır.",
"Reduced draws far terrain coarser: faster on this integrated GPU.":"Azaldılmış ərazini daha qaba çəkir: bu inteqrasiya olunmuş GPU-da daha sürətli.",
"Reduced draws far terrain coarser, for GPUs it slows down.":"Azaldılmış ərazini daha qaba çəkir, GPU-lar üçün isə yavaşlayır.",
"Field of view. Wider shows more terrain at once.":"Baxış sahəsi. Daha geniş bir anda daha çox ərazini göstərir.",
"Caps frames per second; a cap saves power and heat.":"Saniyədə kadrların sayı; qapaq enerjiyə və istiliyə qənaət edir.",
"Waits for the display's refresh: no tearing, a little latency.":"Ekranın təzələnməsini gözləyir: cırılma, bir az gecikmə.",
"Size of menus and the HUD.":"Menyuların ölçüsü və HUD.",
@@ -339,13 +338,11 @@
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.":"Kəsik kənarları hamarlayır. FXAA: daha yumşaq. TAA: ən hamar, hərəkətli şeylər cığır çəkə bilər.",
"Renders the world smaller and rebuilds it with TAA. Faster, softer.":"Dünyanı daha kiçik edir və TAA ilə yenidən qurur. Daha sürətli, daha yumşaq.",
"Sharpens the upscaled picture. Higher: crisper edges, more shimmer.":"Böyüdülmüş təsviri kəskinləşdirir. Daha yüksək: daha aydın kənarlar, daha çox parıltı.",
"HDR output on an HDR display: lava, fire and sunlight shine brighter.":"HDR displeydə HDR çıxışı: lava, yanğın və günəş işığı daha parlaq parlayır.",
"How frames reach the screen while VSync is off.":"VSync söndürüldükdə çərçivələr ekrana necə çatır.",
"Efficient sleeps until the next frame; Precise spins, like vanilla.":"Növbəti kadra qədər səmərəli yuxular; Vanil kimi dəqiq spinlər.",
"Fills the screen (borderless).":"Ekranı doldurur (sərhədsiz).",
"Brightens dark places.":"Qaranlıq yerləri işıqlandırır.",
"Lowers the frame rate when minimised, or idle too.":"Minimumlaşdırıldıqda və ya boş olduqda kadr sürətini azaldır.",
"Sways the view while walking.":"Gəzərkən mənzərəni yelləyir.",
"Fades newly loaded chunks in.":"Yeni yüklənmiş parçaları sönür.",
"Darkens the screen's edges.":"Ekranın kənarlarını qaraldır.",
"Shows when the world saves.":"Dünyanın nə vaxt xilas etdiyini göstərir.",
@@ -556,7 +553,6 @@
"horizon.splitscreen.online_only":"Bu server onlayn rejimdədir: oflayn profillər ona qoşula bilməz, hər oyunçunun öz Minecraft hesabı lazımdır.",
"horizon.splitscreen.own_account":"Bu server hər oyunçunun öz Minecraft hesabını tələb edir",
"horizon.splitscreen.local_only":"Digər oyunçular bu serverə yalnız öz Minecraft hesabları ilə qoşula bilər: oflayn profillər sizin dünyanızda, LAN-da və ya yerli şəbəkənizdəki serverdə oynayır.",
"Hides models behind opaque blocks. Async checks off-thread; entities tick.":"Modelləri qeyri-şəffaf blokların arxasında gizlədir. Async-dən kənar yoxlayır; qurumlar işarələyir.",
"Block Entities":"Blok obyektləri",
"Shulker Boxes":"Shulker qutuları",
"Reuses settled boxes; lid movement stays animated.":"Yerləşmiş qutuları təkrar istifadə edir; qapaq hərəkəti canlı qalır.",
@@ -621,10 +617,6 @@
"horizon.agent.session_opened":"Oyuna başlayan proqram tərəfindən bu sessiya üçün açıldı.",
"horizon.agent.socket":"Soket: %s (tam yolu üçün təfərrüatları kopyalayın)",
"horizon.agent.local_only":"Yalnız yerli: bu kompüterdəki agentlər, heç vaxt şəbəkə.",
"%s nits":"%s nit",
"Auto (%s nits)":"Avtomatik (%s nits)",
"%.2f nits":"%.2f nit",
"Hides models behind opaque blocks. Entities update. Off with custom packs.":"Modelləri qeyri-şəffaf blokların arxasında gizlədir. Müəssisələrin yenilənməsi. Fərdi paketlərlə endirim.",
"Entity Culling Precheck":"Obyekt kəsmə ön yoxlaması",
"Runs the mob AABB precheck after this many loaded mobs. Zero disables it.":"Yüklənmiş mob sayı bu həddə çatanda mob AABB ön yoxlamasını işlədir. Sıfır onu söndürür.",
"Runs the mob AABB precheck after this many loaded mobs. Zero disables it.":"Моб AABB алдан тикшереүен был һандағы тейәлгән мобтан һуң башҡара. Ноль уны һүндерә.",
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.":"Грубая аддаленая мясцовасць у лакальных мірах. Убудаваныя графічныя працэсары спыняюцца на 128 кавалках.",
"Reduced draws far terrain coarser: faster on this integrated GPU.":"Паменшанае малюнак больш грубай мясцовасці: хутчэй на гэтым убудаваным графічным працэсары.",
"Reduced draws far terrain coarser, for GPUs it slows down.":"Зніжэнне малюе больш грубую мясцовасць, для графічных працэсараў гэта запавольваецца.",
"Field of view. Wider shows more terrain at once.":"Поле зроку. Шырэй паказвае адразу больш мясцовасці.",
"Caps frames per second; a cap saves power and heat.":"Колькасць кадраў у секунду; шапка эканоміць энергію і цяпло.",
"Waits for the display's refresh: no tearing, a little latency.":"Чакае абнаўлення дысплея: без разрыву, з невялікай затрымкай.",
"Size of menus and the HUD.":"Памер меню і HUD.",
@@ -339,13 +338,11 @@
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.":"Згладжвае няроўныя краю. FXAA: мякчэй. TAA: самыя гладкія рэчы, якія рухаюцца, могуць цягнуцца.",
"Renders the world smaller and rebuilds it with TAA. Faster, softer.":"Памяншае свет і перабудоўвае яго з TAA. Хутчэй, мякчэй.",
"Sharpens the upscaled picture. Higher: crisper edges, more shimmer.":"Павялічвае рэзкасць павялічанай выявы. Вышэй: больш рэзкія краі, больш мігацення.",
"HDR output on an HDR display: lava, fire and sunlight shine brighter.":"Вывад HDR на дысплеі HDR: лава, агонь і сонечнае святло ззяюць ярчэй.",
"How frames reach the screen while VSync is off.":"Як кадры трапляюць на экран, калі VSync выключаны.",
"Efficient sleeps until the next frame; Precise spins, like vanilla.":"Эфектыўнае спіць да наступнага кадра; Дакладныя спіны, як ваніль.",
"Fills the screen (borderless).":"Запаўняе экран (без палёў).",
"Brightens dark places.":"Асвятляе цёмныя месцы.",
"Lowers the frame rate when minimised, or idle too.":"Зніжае частату кадраў пры згортванні або ў рэжыме чакання.",
"Sways the view while walking.":"Качае погляд падчас хады.",
"Fades newly loaded chunks in.":"Змяншае толькі што загружаныя кавалкі.",
"Darkens the screen's edges.":"Зацямняе краю экрана.",
"Shows when the world saves.":"Паказвае, калі свет ратуе.",
@@ -556,7 +553,6 @@
"horizon.splitscreen.online_only":"Гэты сервер у анлайн-рэжыме: афлайн-профілі не могуць далучыцца, кожнаму гульцу патрэбны ўласны ўліковы запіс Minecraft.",
"horizon.splitscreen.own_account":"Гэтаму серверу патрэбны ўласны ўліковы запіс Minecraft для кожнага гульца",
"horizon.splitscreen.local_only":"Іншыя гульцы могуць далучыцца да гэтага сервера толькі са сваім уліковым запісам Minecraft: афлайн-профілі гуляюць у вашым свеце, па LAN або на серверы ў вашай лакальнай сетцы.",
"Hides models behind opaque blocks. Async checks off-thread; entities tick.":"Хавае мадэлі за непразрыстымі блокамі. Async правярае па-за патокам; суб'ектаў цік.",
"Block Entities":"Блокавыя аб'екты",
"Shulker Boxes":"Скрынкі Шулкера",
"Reuses settled boxes; lid movement stays animated.":"Паўторна выкарыстоўвае адстойнікі; рух вечка застаецца ажыўленым.",
@@ -621,10 +617,6 @@
"horizon.agent.session_opened":"Адкрыты для гэтай сесіі праграмай, якая запусціла гульню.",
"horizon.agent.socket":"Сокет: %s (скапіруйце дэталі для поўнага шляху)",
"horizon.agent.local_only":"Толькі лакальна: агенты на гэтым кампутары, ніколі ў сетцы.",
"%s nits":"%s ніт",
"Auto (%s nits)":"Аўта (%s ніт)",
"%.2f nits":"%.2f ніт",
"Hides models behind opaque blocks. Entities update. Off with custom packs.":"Хавае мадэлі за непразрыстымі блокамі. Абнаўленне суб'ектаў. Выключыце спецыяльныя пакеты.",
"Runs the mob AABB precheck after this many loaded mobs. Zero disables it.":"Выконвае папярэднюю праверку AABB мабаў пасля гэтай колькасці загружаных мабаў. Нуль адключае яе.",
"%s mobs":"%s мабаў",
Loaded 100 of 1149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.