Compare commits

...
Author SHA1 Message Date
Kacper 10b0942e4f Merge pull request #10191 from KBroneZ/claude/fix-10159-empty-noise
upgrade-datapack: a noise with no octaves becomes a silent 26.3 noise
2026-11-06 08:55:56 +01:00
Kacper 81aeeacf55 Merge pull request #10522 from kacpherek/perf/smp-server
Server: boats and minecarts stop walking every mob, and a Perfetto trace of every thread
2026-11-06 08:54:22 +01:00
Kacper a0261a708a Merge pull request #10190 from KBroneZ/claude/fix-10158-default-states
Read default_block and default_fluid in 26.3's block state form
2026-11-06 08:52:19 +01:00
Kacper 894f7a0684 Merge pull request #10167 from DUzzL/fix/10113-f3-keys-main-menu
Handle F3 debug keys on the title screen
2026-11-06 08:52:13 +01:00
Kacper d5b1c59f67 Merge pull request #10507 from DUzzL/fix/10447-lead-creative-spectator
Keep the lead in creative and drop mob leads when the holder spectates
2026-11-06 08:52:07 +01:00
Kacper b0ea840d13 Merge pull request #10128 from DUzzL/fix/10048-ladder-mob-push
Mobs on a ladder or vine are not pushable; no crit while climbing
2026-11-06 08:52:00 +01:00
Kacper 479d2082a8 Merge pull request #10187 from DUzzL/fix/10073-horse-jump-anim
Fix horse rear ending before a donkey or mule lands
2026-11-06 08:51:52 +01:00
Kacper d4b24f27d6 Merge pull request #10206 from DUzzL/fix/10157-shulker-tooltip
Fix numbered translation args in container item tooltips
2026-11-06 08:51:46 +01:00
Kacper 3ad30a2021 Merge pull request #10488 from AlfaRojo/fix/crossbow-flame-ignite
Crossbows: Flame ignites the arrows they shoot
2026-11-06 08:51:38 +01:00
Kacpherek f80bf5d7f3 Vehicles: test a mob's position before working out its box
A boat's or minecart's box query (boat_contacts, boat_obstacles,
cart_entity_collisions) asked every mob of the level for its box, and
Mob::bounds reads the mob's whole size (dims: sleeping, baby, species, scale)
across several cache lines. While boats and carts tick, the widest half width
and tallest height of the mobs (BoxReach) are worked out once, and a mob whose
position is farther than that from the query is skipped without its box. The
reach is worked out again when a mob joins or leaves, and nothing a vehicle
does resizes a mob, so the same mobs are found.
2026-11-06 08:36:08 +01:00
Ludovic Marie 0d21d5520e Merge pull request #10477 from Detimes/feat-10404-older-clients-1a
🤖 Agent: a 26.2 client is now refused in configuration before any untranslated registry data, tags or play packets (bebf670); 26.3 path byte-for-byte unchanged with the option off
2026-11-06 08:27:12 +01:00
Kacpherek 02bdb03444 Vehicles: seat riders and find boat contacts without a pass over every mob per vehicle
position_riders finds every vehicle's riders in one pass over the mobs instead
of one per boat, minecart and primed TNT. A boat tests a mob's box before
anything else, and works out the scoreboard teams only when something touches
it, instead of once per boat a tick.
2026-11-06 08:10:31 +01:00
Kacpherek c7183d6232 Trace: every thread's spans as a timeline Perfetto opens
HORIZON_TRACE=FILE, horizon-ctl trace FILE [SECONDS] and horizon-trace on a
dedicated server's console write each span of every thread as a Chrome trace
event, so what ran beside what, and what a thread waited for, can be read off
in ui.perfetto.dev. A thread that records spans but is never taken from (a far
terrain or lighting worker while spans are on) now drops them past 16k instead
of growing for as long as spans stay on.
2026-11-06 08:10:30 +01:00
Ludovic Marie 98af9399cd Merge pull request #10509 from horizon-rust/perf/launcher-prepared-cache
🤖 Agent: is_prepared now answers from a 5 s per-version cache (21.4 µs to 0.21 µs per call); need_prepared and the CLI still run the full per-file check, so an empty JSON is caught before launch.
2026-11-06 07:53:06 +01:00
Horizon agentandClaude Opus 5.5 bebf670623 Turn a 26.2 client away before untranslated registry data
Only packet ids are translated for an older client so far (#10404): the
registry data, tags (registry ids) and play packets (chunks, light, login,
player_action numbering...) would reach a real 26.2 client in 26.3 ids and
layouts. With accept_older_clients on, such a client now logs in and is
told so in configuration, right after the known packs and before any of
them. NetConfig::older_clients_play (tests only, not in server.toml) keeps
the play path reachable for the id renumbering test until the later parts
translate the contents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:51:40 +01:00
Horizon agent 5daf0853e3 Merge remote-tracking branch 'origin/main' into review-10477 2026-11-06 07:44:24 +01:00
Horizon agentandClaude Sonnet 5.5 bb8a9d9524 Launcher: pages drawn ask "is it prepared?" from memory, not the disk
#10389 made is_prepared parse research-lock.json and the manifest, list the
prepared folder and stat every table on each call. Pages call it while drawing
(instance rows, the Play menu, can_start), so every frame did file I/O.

is_prepared now keeps its answer per version folder for 5 s and drops it when
the launcher installs, removes, reuses resources or a job exits.
check_prepared runs the full check and refreshes the answer; starting the game
or a server (need_prepared), the CLI and the post-prepare auto-play use it, so
an empty JSON is still caught before a launch.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 07:44:15 +01:00
Ludovic Marie 3a81751180 Merge pull request #10458 from TheMrZZ/fix/h028-stand-fall-save
🤖 Agent: Entity.checkFallDamage subtracts (float)ya into the double, which the f64::from(.. as f32) step reproduces, and Entity.load reads fall_distance back. Main merged in again after #10422 landed (GitHub read the criss-cross bases as a conflict); the diff to main is this PR's own 6 files.
2026-11-06 07:28:07 +01:00
Horizon-agentandClaude Opus 5.5 ec72efe92d Merge main into #10458 after #10422 landed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:25:35 +01:00
Ludovic Marie 87c51ab9c9 Merge pull request #10471 from arslanarm/shaderpacks/shadow-pass-block-entities
🤖 Agent: `camera_list` is now collected into a Vec on every frame, even with no pack, where the old iterator was lazy; it's one small allocation per frame. The shadow-only set and the spawner's `shadow_pass_drew` scan (linear in shadow_visible per spawner) run only under a pack whose shadow draws entities.
2026-11-06 07:23:45 +01:00
Kacper 108c130237 Merge pull request #10340 from CocofireHD/fix/10327-docs-readme-md-index-the-benchmark-and-perfo
docs: index benchmark and performance result pages in docs/README.md
2026-11-06 07:18:18 +01:00
Kacper 73fdce3c01 Merge pull request #10375 from voidsipp/fix/10326-docs-index
Index the missing design, server and release docs
2026-11-06 07:18:10 +01:00
Kacper bf22a35ab3 Merge pull request #10350 from xxaxio/docs/10325-index-player-launcher-docs
docs/README.md: index missing player, launcher and platform pages
2026-11-06 07:18:04 +01:00
Kacper e41b6599ec Merge pull request #10351 from xxaxio/docs/10324-remove-stale-not-ported-notes
docs: remove stale "not ported" notes
2026-11-06 07:17:58 +01:00
Kacper 0c2aec59f6 Merge pull request #10234 from KBroneZ/claude/fix-10164-forceload
Load chunks in /forceload add, as vanilla does
2026-11-06 07:17:48 +01:00
Kacper 50c2ade6c6 Merge pull request #10233 from KBroneZ/claude/fix-10163-template-loot
Keep a placed template's container loot table
2026-11-06 07:17:42 +01:00
Kacper 3281781334 Merge pull request #10370 from xxaxio/fix/10322-map-can-place-on-banner
game/maps: honour can_place_on when using a map on a banner in adventure mode
2026-11-06 07:17:35 +01:00
Kacper 5871fa922f Merge pull request #10278 from Joansitoh/fix/10044-underwater-sky
Stop the underwater sky scan on kelp and seagrass
2026-11-06 07:17:25 +01:00
Kacper 16bb8b3e65 Merge pull request #10277 from Joansitoh/fix/10044-bucket-liquid
Replace a bubble column when a bucket empties into it
2026-11-06 07:17:18 +01:00
Kacper 1e35996644 Merge pull request #10274 from DUzzL/fix/10250-zip-end-record-disk
Normalize zip END records with equal non-zero disk numbers
2026-11-06 07:17:12 +01:00
Ludovic Marie 499b049803 Merge pull request #10422 from TheMrZZ/fix/h046-loot-pickup-save
🤖 Agent: Mob.addAdditionalSaveData always writes canPickUpLoot(), so moving it out of the put-if-absent defaults is the fix (Allay and sulfur cube overrides now win over a loaded byte). Main merged in after #10393 landed; clippy and the entity_nbt tests pass on the merge.
2026-11-06 07:16:40 +01:00
Ludovic Marie 29825eb774 Merge pull request #10411 from DUzzL/fix/10318-piglin-gear-compare
🤖 Agent: piglin_replace follows Piglin.canReplaceCurrentItem in order (PREVENT_ARMOR_CHANGE first, then wanted/unwanted, then Mob.canReplaceCurrentItem), with getPreferredWeaponType null for babies.
2026-11-06 07:12:21 +01:00
Horizon-agentandClaude Opus 5.5 ded0d1a283 Merge main into #10458
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:11:56 +01:00
Horizon-agentandClaude Opus 5.5 1c8729793f Merge main into #10422
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 07:11:51 +01:00
Ludovic Marie 69c1400e58 Merge pull request #10389 from CocofireHD/fix/10303-my-game-crash-after-launch
🤖 Agent: `is_prepared` now reads the manifest and stats every top-level JSON on each call, and the launcher calls it from render paths (glass_home, glass_palette, can_start): a few hundred µs per frame at most, but a cache keyed on the folder would remove it. I dropped the unused `is_json_file`; launcher clippy and backend tests pass.
2026-11-06 07:11:07 +01:00
Ludovic Marie 948c6d6a88 Merge pull request #10438 from megabytesme/fix/windows-cross-compile-icon
🤖 Agent: checked on Linux with mingw: the build script now compiles `main_res.o` for x86_64-pc-windows-gnu and passes it to the link, which the host-gated `#[cfg(windows)]` skipped. I kept the warn-only fallback outside CI for windres paths with a space (#10398).
2026-11-06 07:11:03 +01:00
Ludovic Marie b8c058a249 Merge pull request #10386 from voidsipp/fix/10355-boat-attack-knockback
🤖 Agent: causeExtraKnockback's non-living branch is Entity.push (no knockback resistance, no halving), as strike_boat does; the guest path still checks the attack range before skipping look_at_entity. We merged #10434 into this branch: both extend the boat fixture and harness.
2026-11-06 07:10:59 +01:00
Ludovic Marie 5a0c5d522f Merge pull request #10437 from voidsipp/fix/10401-snbt-float-text
🤖 Agent: for one-digit mantissas {:.1e} picks the closest two-digit decimal, which is what Java's Float/Double.toString does (Float.MIN_VALUE prints 1.4E-45). We switched the harness to the shared release world_probe: debug at CARGO_BUILD_JOBS=1 would have compiled horizon-world again in CI.
2026-11-06 07:10:55 +01:00
Ludovic Marie ff27525aaf Merge pull request #10448 from masa-masa-0314/fix/realms-add-realm-strings
🤖 Agent: RealmsPdpScreen shows the trial description (and a Try for free button) only when trialAvailable; Horizon does not ask for it, so we dropped the description and kept the title with Buy, vanilla's no-trial layout. Titles are drawn plain where vanilla underlines/bolds them: small follow-up.
2026-11-06 07:10:52 +01:00
Ludovic Marie 54112649da Merge pull request #10413 from DUzzL/fix/10319-golem-reset-anger
🤖 Agent: start() matches NeutralMob.stopBeingAngry plus IronGolem.startPersistentAngerTimer (nextInt(381) + 400 = rangeOfSeconds(20, 39)) at target priority 4. We moved the creeper test's doc comment back onto its own test: the new test had been inserted between them.
2026-11-06 07:10:48 +01:00
Ludovic Marie ef207a7c92 Merge pull request #10391 from masa-masa-0314/feat/realms-tier-distance
🤖 Agent: the tier fetch and both PUTs hit the same paths as RealmsClient.getRealmTierConfiguration/updateRenderDistance/updateSimulationDistance, and a reply missing either range is an error as in vanilla; nothing beyond what the vanilla settings tab offers.
2026-11-06 07:10:43 +01:00
Ludovic Marie c86e080406 Merge pull request #10388 from voidsipp/fix/conversion-command-tags
🤖 Agent: only Tags move to the new id, as ConversionType.convertCommon copies them; the old raw compound (UUID, species data) is left behind, which the save/reload test checks.
2026-11-06 07:10:39 +01:00
Ludovic Marie 26243ba3b7 Merge pull request #10392 from voidsipp/fix/7436-fishing-ground-motion
🤖 Agent: hook_block_hit now runs on the client before moving, as FishingHook.checkCollision does on both sides, and gravity is f64::from(0.03f32) (getDefaultGravity is a float).
2026-11-06 07:10:35 +01:00
Ludovic Marie a4f021ec67 Merge pull request #10414 from voidsipp/fix/4832-remote-pig-steering
🤖 Agent: matches Pig/Strider.getControllingPassenger (saddled, first passenger a player holding the right stick in either hand); the server side reads the same rule through moved_by_client, so it no longer simulates the mount twice.
2026-11-06 07:10:31 +01:00
Ludovic Marie a7bf97e67d Merge pull request #10417 from voidsipp/fix/next-1-glint-override
🤖 Agent: component_bool matches NbtOps.getBooleanValue (doubleValue != 0, so 0.5f and NaN are true, Short 256 too), not NumericTag.asBoolean's byteValue; that is the Hypixel glint_override report.
2026-11-06 07:10:28 +01:00
Ludovic Marie 11e2482ae5 Merge pull request #10426 from voidsipp/fix/levitation-fall-distance
🤖 Agent: LivingEntity.aiStep resets fall distance for slow_falling or levitation before travel, NoAI included; dropping the later travelInAir-style reset is right, it erased the current tick's descent.
2026-11-06 07:10:24 +01:00
Ludovic Marie 363906684c Merge pull request #10430 from voidsipp/fix/10397-packet-utf8-replacement
🤖 Agent: packet_utf8 differs from from_utf8_lossy only on ED A0..BF (encoded surrogates), which Java consumes as one replacement; the length limits still use the encoded bytes first and UTF-16 units after, as Utf8String.read does.
2026-11-06 07:10:20 +01:00
Ludovic Marie a567b64f20 Merge pull request #10433 from voidsipp/fix/10399-unreadable-moderation-lists
🤖 Agent: reading the bytes once and parsing them removes the exists()-then-load race too; only NotFound starts an empty list, so a directory or a locked ops.json now stops the stopped-server command instead of being overwritten.
2026-11-06 07:10:16 +01:00
Ludovic Marie faa7e2746f Merge pull request #10407 from DUzzL/fix/10323-evoker-fangs-teams
🤖 Agent: fangs_allied follows Entity.isAlliedTo both ways (AbstractIllager tag only when both are teamless, Evoker's vex root-owner rule); guests on the evoker's team are still bitten, as before, which is a follow-up rather than a regression.
2026-11-06 07:10:12 +01:00
Horizon-agent b4197ddd94 Merge remote-tracking branch 'origin/main' into w-10471
# Conflicts:
#	crates/horizon-client-render/src/gfx.rs
2026-11-06 07:08:12 +01:00
Ludovic Marie ad8663977c Merge pull request #10459 from TheMrZZ/fix/armor-stand-command-equip-events
🤖 Agent: follows LivingEntity.onEquipItem (same item+components or firstTick: nothing; EQUIPPABLE on the new stack picks equip over unequip) and data merge keeps first_tick. We removed the docs/parity run-log file.
2026-11-06 07:03:36 +01:00
Ludovic Marie 0b3a4bda9c Merge pull request #10424 from TheMrZZ/fix/hostile-sound-categories
🤖 Agent: all nine families are Monster subclasses (entity.hostile.* is Monster's shared hurt/death/fall set), so HOSTILE is right; the list scan is 34 short strings per sound, negligible. We removed the docs/parity run-log file.
2026-11-06 07:03:32 +01:00
Ludovic Marie 5fb8f59ea0 Merge pull request #10432 from TheMrZZ/fix/numeric-damage-component
🤖 Agent: DataComponents.DAMAGE goes through Codec.INT (Number.intValue: truncation, Long keeps its low 32 bits), and dynamic::as_int already does that. We removed the docs/parity run-log file.
2026-11-06 07:03:29 +01:00
Ludovic Marie 62d2b18c16 Merge pull request #10400 from TheMrZZ/fix/inline-fishing-tool-context
🤖 Agent: LootCommand.dropFishingLoot's no-tool endpoint passes ItemStack.EMPTY, and the code only reads tool_stack when a tool, mainhand or offhand was given, so an omitted tool stays empty. We removed the docs/parity run-log file.
2026-11-06 07:03:25 +01:00
Ludovic Marie 17541e8bff Merge pull request #10387 from TheMrZZ/fix/baby-pig-passenger-attachment
🤖 Agent: Pig BABY_DIMENSIONS has PASSENGER (0, 0.5, 0), so the one match arm is right. We removed docs/parity/baby-pig-passenger-attachment.md: run logs and hashes belong in the PR description, not in docs/.
2026-11-06 07:03:22 +01:00
Kevin RamirezandClaude Fable 5.1 927b546c9a Crossbows: Flame ignites the arrows they shoot (#10482)
`Game::shoot_crossbow` set the arrow's weapon and Piercing count but never
ran the weapon's `projectile_spawned` ignite, which only the bow path
applied. In vanilla, `AbstractArrow`'s constructor runs
`EnchantmentHelper.onProjectileSpawned` for every arrow built with a
weapon, bow or crossbow, so a Flame crossbow shoots burning arrows that
prime TNT and set entities on fire.

The bow's `bow_projectile_ignite` becomes `ignite_spawned_arrow(weapon,
arrow)`, shared by both weapons; the crossbow ignites each arrow of its
volley after Piercing, as the constructor does. The port map rows follow
the rename, and a resource-backed regression checks no Flame, Flame and
Flame with Multishot.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-11-06 00:02:20 -06:00
Ludovic Marie fff2a81e41 Merge pull request #10395 from DUzzL/fix/10321-spawnpoint-bubble-column
🤖 Agent: confirmed in Blocks.<clinit>: water, lava and bubble_column are the only three blocks with .liquid(), so the name list is complete for Block.isPossibleToRespawnInThis.
2026-11-06 07:02:01 +01:00
Ludovic Marie 1f883e7a79 Merge pull request #10402 from Skr1zzzz/server-tick-spans
🤖 Agent: `tick_blocks` starts a fresh `laps` after the scheduled ticks, so the stretch between `tb_entity_inside` and `tb_broadcast` shows only through `random_ticks`, `tb_scheduled` and `tb_fluids`. The counters and laps cost one relaxed load each when spans are off.
2026-11-06 07:01:57 +01:00
Ludovic Marie c956e11ff8 Merge pull request #10470 from DUzzL/fix/10441-ghastling-sounds
🤖 Agent: HappyGhast.getHurtSound/getDeathSound pick GHASTLING_* by isBaby, and happy_ghast::sounds already does exactly that, so the new voice_sounds arm is the whole fix.
2026-11-06 07:01:53 +01:00
Ludovic Marie f6a9444deb Merge pull request #10416 from Skr1zzzz/spawn-parity
🤖 Agent: checked against 26.3: `ChunkAccess.getHeight` returns `getFirstAvailable - 1` and `getRandomPosWithin` adds the 1 back, so topEmptyY is Horizon's `world_surface` itself. The old +1 made the bound 130 for a column solid to y 63. run-parity's natural_spawn harness checks the draws end to end.
2026-11-06 07:01:50 +01:00
Ludovic Marie 0610b830ca Merge pull request #10434 from voidsipp/fix/4545-boat-paddle-transition
🤖 Agent: rowing_time now gates on paddle_controller like AbstractBoat.getPaddleState (flag AND a controlling passenger) and clamps a as Mth.clampedLerp does; #10386 carries this branch merged, queued after it.
2026-11-06 07:01:46 +01:00
Ludovic Marie 8a21328406 Merge pull request #10439 from Skr1zzzz/random-ticks
🤖 Agent: the `[false; 256]` table relies on a local palette never holding more than 256 entries (global palettes keep the per-block path). The test covers 1, 2, 256 and 300 states, a stale palette entry, and the LCG draws against an independent oracle.
2026-11-06 07:01:39 +01:00
Ludovic Marie d165254d41 Merge pull request #10393 from TheMrZZ/fix/h011-orb-saved-uuid
🤖 Agent: matches Entity.saveWithoutId: the orb's command NBT now reads its registered uuid_for(id) instead of a fresh random UUID per read; queued first of the 10393/10422/10458 stack (the other two carry this branch merged).
2026-11-06 07:01:34 +01:00
Ludovic Marie 0125ca5281 Merge pull request #10435 from Hiuoy/feat/model-layers
🤖 Agent: `native_bake_layer` re-reads entity-models.json on every call for a vanilla layer no mob has (the doc says so), so a mod that bakes per frame would hitch; caching that document is the natural next step. The humanoid-against-zombie#main bake test is the right oracle, and the fragment stamps cleanly (tests.test_stamp_native_hooks).
2026-11-06 07:01:29 +01:00
Ludovic Marie 743d6229fe Merge pull request #10419 from senjinthedragon/resize-free-first
🤖 Agent: the early return for an unchanged size stays ahead of the new `device.poll(wait)`, so the GPU sync happens once per real size change. A drag-resize pays it per new size, a fair price for not holding both sets of buffers.
2026-11-06 07:01:25 +01:00
Ludovic Marie 6df8d31833 Merge pull request #10456 from arslanarm/shaderpacks/smooth-across-teleports
🤖 Agent: `world_seen` never resets inside a `Custom`, so the restart on entering the Nether relies on the pack being rebuilt for each dimension. If `PackPost` is ever kept across dimensions, it will need resetting there.
2026-11-06 07:01:21 +01:00
Ludovic Marie cc4911fc2b Merge pull request #10457 from arslanarm/shaderpacks/colortex-mips-blend
🤖 Agent: only `Source::Color` gets the linear-mip sampler; shadowcolor, custom textures and the lightmap keep `Linear` (nearest level), which matches what was measured. Buffers without mipmaps read the same as before.
2026-11-06 07:01:17 +01:00
Ludovic Marie 8d1a46ef24 Merge pull request #10467 from arslanarm/shaderpacks/shadow-depth-filter-precision
🤖 Agent: the 256ths weights are a fit to one NVIDIA GPU but now apply on every GPU, so an AMD or Intel reading of Iris may move them later. The cost is two floors and a multiply per filtered shadow read.
2026-11-06 07:01:13 +01:00
Ludovic Marie a6a228a193 Merge pull request #10469 from arslanarm/shaderpacks/shadow-pass-spectator
🤖 Agent: the spectator rule keys on `pack_ids.is_some()`, so a native pack's shadow keeps the first-person model, as the test pins. The other game modes stay as before, since only spectator was measured.
2026-11-06 07:01:09 +01:00
Ludovic Marie 57140bb20f Merge pull request #10473 from arslanarm/shaderpacks/terrain-vertex-grid
🤖 Agent: the +1/32 bias moves only places within one float step under a 32768th line, so #9839's water tops (29094.34) and 13/15 offsets (28398.93) still floor as before, and the new test case pins the 1, 0.5 and 9.5 edges.
2026-11-06 07:01:05 +01:00
Ludovic Marie 0c7ddd45c5 Merge pull request #10436 from arslanarm/shaderpacks/shadow-pass-depth-culling
🤖 Agent: with the near and far planes dropped, the shadow pass now draws every section inside the box's sides out to its reach, so a pack with tight planes draws more shadow terrain than before. Worth watching in the cave perf numbers. #10471 conflicts with this in gfx.rs; I'll merge main into it once this lands.
2026-11-06 07:01:00 +01:00
Horizon-agentandClaude Opus 5.5 c9b0011952 Merge #10434 (boat paddles) into #10386
Both extend the boat authority fixture and harness: keep the paddle and
attack cases, both Case fields and both docstring paragraphs. The harness
keeps main's fast-profile build with #10434's --resources option.
Queued after #10434, so either landing first leaves the other mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:57:38 +01:00
Horizon-agentandClaude Opus 5.5 a25a11fbf7 Merge #10422 and #10393 into #10458
All three add a command_exec fixture group at the same lines and a test
module at the end of entity_nbt.rs; keep every group, flag, dispatch block
and module. Queued in order so whichever lands first leaves the others
mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:56:29 +01:00
Horizon-agentandClaude Opus 5.5 9f9daee867 Merge #10393 (orb saved UUID) into #10422
Both add a command_exec fixture group at the same lines; keep both groups,
their isolation sets, flags and dispatch blocks. Queued in order so either
landing first leaves the other mergeable.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:55:28 +01:00
DUzzL 468a73f128 Keep the lead in creative and break mob leads when the holder spectates 2026-11-06 06:54:41 +01:00
Horizon-agentandClaude Opus 5.5 c542322582 Float text harness: build the shared release world_probe
A debug world_probe at one job compiles horizon-world from scratch beside
the release probe every other horizon-world harness uses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:54:15 +01:00
Horizon-agentandClaude Opus 5.5 264d5f38c7 Realms product page: the trial header without its description
Horizon does not ask trialAvailabilityTask. Vanilla shows the trial
description (and offers the trial in the footer) only when one is
available; otherwise the header is the title alone and the footer offers
Buy, which is what this screen shows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:53:56 +01:00
Horizon-agentandClaude Opus 5.5 dd23018283 Keep the creeper test's doc comment on its own test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:53:11 +01:00
Horizon-agentandClaude Opus 5.5 3b575a0016 Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:48 +01:00
Horizon-agentandClaude Opus 5.5 94d295d20c Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:45 +01:00
Horizon-agentandClaude Opus 5.5 ec080faa9f Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:42 +01:00
Horizon-agentandClaude Opus 5.5 1d1b060e1a Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:37 +01:00
Horizon-agentandClaude Opus 5.5 5e8d9a5123 Drop the run-log document from the PR
The PR description already holds the vanilla reference and the measured
results; docs/ keeps player and contributor documentation only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:52:26 +01:00
Horizon-agentandClaude Opus 5.5 3649dfb9eb json_file: drop is_json_file, which nothing calls
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:45:51 +01:00
Horizon-agentandClaude Opus 5.5 94d8386027 build.rs: keep the no-icon fallback for local builds
The GNU windres the launcher's native mod builds use can't take a path with a
space, so a local build there must go on without the icon (#10398 asked to keep
it); CI still fails without it. One Windows block for the stack and the icon.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 06:45:50 +01:00
Arslan ArmanulyandClaude Opus 5.5 c9bc6104b3 Shader packs: a terrain vertex a float step under a 32768th sits on it
A shader pack's terrain gl_Vertex floors each vertex's place in its
section to a 32768th. Plants whose random offset is 9/15 of a block came
out a 32768th under a half block, where our probe packs read Iris
1.11.7's exactly on it: we guess, untested, that our float sums put them
a hair under the line. A place up to 1/32 of a 32768th under a grid line
is now on it.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:26:00 +05:00
Arslan ArmanulyandClaude Opus 5.5 02c05de455 Shader packs: shadowtex filters in whole 256ths, as our Iris readings fit
A pack's texture() on shadowtex0/1 through a plain sampler blended the
four texels with the exact fraction. Iris 1.11.7's filtered reads on our
NVIDIA GPU fit weights in whole 256ths: the fraction rounded half up on
each axis, the far corner's weight their product over 256 rounded half
up, the others the rest of each axis. #8464's lit probe then differs
from Iris's at no pixel of ow_ground_noon.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:52 +05:00
Arslan ArmanulyandClaude Opus 5.5 6a737285e8 Shader packs: the shadow pass takes the block entities of the sections it draws
A shader pack's shadow pass drew the block entities the camera's view
had built: a chest beside the view, in a section whose terrain the
shadow pass draws, was left out, and a spawner's mob out of the view
and out of those sections went in. Our probe packs' Iris 1.11.7 shadow
maps hold a chest wherever the shadow pass draws its section's terrain
and nothing of the one spawner in our scenes, which lies outside them.
The block entities of the sections the shadow pass drew last frame are
now added hidden from the camera, and a spawner's mob that is out of
the view and out of those sections is left out.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:51 +05:00
Arslan ArmanulyandClaude Opus 5.5 1354db6f08 Shader packs: a spectator's own model stays out of the shadow pass, as in Iris
In first person, a shader pack's shadow pass drew the spectator's own
model (its head) at the camera. Our probe packs' readings of Iris
1.11.7's shadow map in spectator mode hold the terrain below the camera
there.

Part of #8464

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 10:25:42 +05:00
Detimes d503874bff Let 26.2 clients join a Horizon server, off by default: handshake and packet ids
A server.toml option, [server] accept_older_clients (horizon-accept-older-clients), off
by default. On, a login handshake stating 776 opens an adapter::ClientSession on the
connection instead of being refused, and a 26.2 status ping is told its own protocol.
The session follows the connection's states as Session does, mirrored, and renumbers
packet ids both ways with the inverse of the generated 26.2 tables, in the blocking
login path and in the io_pool play path. Off, nothing changes.

Part of #10404.
2026-11-06 05:22:16 +00:00
Florian ERNSTandCodex f97e4b9527 Emit game events for armor-stand command equipment changes
Co-Authored-By: Codex <[email protected]>
2026-11-06 06:12:01 +01:00
TheMrZZandCodex gpt-6.1-sol 504b987937 Save armor stands' actual fall distance
Adopt and project the existing fall counter, including Java's per-movement float narrowing before double accumulation. Preserve travel and landing-call order, and cover the real tick/save route plus decoded saves without raw data.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 05:59:09 +01:00
masa-masa-0314andZCode c81874d772 Realms: the settings sliders' tooltips
`createDistanceOption`'s cached constant tooltips, shown while the
pointer is over either distance slider.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 12:55:30 +08:00
Ludovic Marie a589a3dd9b Merge pull request #10449 from horizon-rust/fix/native-stale-engine-target
🤖 Agent: staged files now get the current time, so a Nightly's source unpacked with its commit's older mtimes can no longer pass for already-compiled crates (Windows/macOS fs::copy kept them); one-time stage rewrite for old stages.
2026-11-06 05:53:41 +01:00
Arslan ArmanulyandClaude Opus 5.5 8d38356bf4 Shader packs: smooth() keeps going through teleports, as in Iris
A custom uniform's smooth() restarted on every frame whose camera biome was
unknown, so after each teleport it started again, and it started from the
unknown frame's values (biome -1, biome_category 0, temperature 0): the
frame that restarted it also stored them, and the first frame that knew the
biome went on from there.

Now smooth() restarts only until the first frame that knows the camera's
biome, that frame included, and then keeps going for as long as the pack
runs. With a probe pack of ours, Iris 1.11.7 kept smoothing across
teleports within a dimension and started again when the player entered
the Nether.

Fixes #10450
Part of #9307

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:53:24 +05:00
Arslan ArmanulyandClaude Opus 5.5 4c0ef1d7df Shader packs: colortex mipmaps blend their two nearest levels, as in Iris
A shader pack's colortex read between two mipmap levels (textureLod at a
fractional level) took the nearest level: the buffers' sampler had the
default, nearest, mipmap filter. In Iris 1.11.7 our probe packs read every
colortex, in composite and in final, as the two nearest levels mixed. The
buffers now get a sampler of their own that blends them; shadowcolor and
the other textures keep theirs (not measured).

Fixes #10451
Part of #9307

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:53:24 +05:00
Horizon agentandClaude Opus 5.5 4b24e21a8a launcher: format the stamping test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:49:06 +01:00
DUzzL ebafc91999 Use ghastling hurt and death sounds for baby HappyGhasts 2026-11-06 05:47:17 +01:00
Horizon agentandClaude Opus 5.5 1fe1d5e860 launcher: stamp staged native source files with the current time
A Nightly's source keeps its commit's mtimes when unpacked, and fs::copy
keeps them on Windows and macOS, so a changed engine file reached
native/stage/ older than the previous build and Cargo reused the old
horizon-core. sync now sets each written file's mtime to now; a stage
written before this is rebuilt once.

Fixes #10421

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 05:41:23 +01:00
masa-masa-0314andZCode 382f264b6a Realms: the Add Realm screen as vanilla's product page
The screen behind Add Realm is 26.3's `RealmsPdpScreen`: the header's
logo, the trial header, then the friends, minigames and private-server
entries with their 80x45 pictures and 182-wide text, scrolling between
the header and the footer, whose buttons are the PDP's own title (the
buy link) and Back.

Its old strings were dead: `mco.title` never existed, and 26.3's
deprecated.json removes `mco.selectServer.popup` and
`mco.selectServer.buy`, so the screen drew raw keys.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 12:39:31 +08:00
Ludovic Marie 679f8c6e3f Merge pull request #10440 from horizon-rust/fix/options-emote-key
🤖 Agent: options.txt is vanilla's key list again; the emote key is saved as emoteKey in horizon-extras.txt and an old key_key.emote line is migrated, options parity passed on the desktop.
2026-11-06 05:18:23 +01:00
Horizon agentandClaude Sonnet 5.5 c6d5769e4f Options: rustfmt keybinds.rs
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 05:13:24 +01:00
Ben 1ad899c7b5 fix: embed Windows icon resources for cross builds 2026-11-06 04:13:14 +00:00
voidsip b9439b3508 Match Java text for tiny floats and doubles 2026-11-05 22:12:54 -06:00
Arslan ArmanulyandClaude Opus 5.5 6a8ea6fc18 Shader packs: the shadow pass draws terrain outside the pack's near and far planes
Our probe pack's Iris 1.11.7 shadow maps hold the terrain at every depth we
put it along the light (from 535 blocks beyond the shadow camera to about 120
before it), whatever shadowNearPlane and shadowFarPlane the pack sets, or
none. Horizon dropped the sections outside the shadow projection's near and
far planes; since #9768 those are the pack's own planes with no offset, so
in our probe's cave scene with the planes at 0.05 and 256 the surface toward
the light was missing from the shadow map. A shaders/ pack's shadow pass now
culls by the box's sides only; Horizon's own packs keep their planes.

Fixes #10403

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 09:06:26 +05:00
Florian ERNSTandCodex 1e1c6a900f Decode numeric damage components with Codec.INT semantics
Co-Authored-By: Codex <[email protected]>
2026-11-06 05:03:08 +01:00
voidsip ff381b5dc4 fix(server): reject unreadable lists in stopped moderation commands 2026-11-05 22:02:46 -06:00
HiuoyandClaude Opus 5.5 7dc8ba568f Native mods: entity model layers built in code, baked and drawn (#10425)
A NeoForge mod builds its entity models in code (createBodyLayer: a
MeshDefinition of CubeListBuilder boxes at PartPoses, a LayerDefinition),
registers them in EntityRenderersEvent.RegisterLayerDefinitions, bakes them
with EntityModelSet.bakeLayer and draws the ModelPart tree it posed in
setupAnim with ModelPart.render. Native mods get the same:

- the builders and ModelPart, ported from vanilla (mobs/model_layers.rs):
  box UV from texOffs, mirror, deformation, texture scale and visible faces
  as ModelPart.Cube bakes them; poses, reset_pose, get_child;
- native_layer_definitions, a collect hook asked once at start, keyed by
  ModelLayerLocation ids in the mod's own namespace;
- native_bake_layer: a registered layer, or vanilla's by its
  entity-models.json name (minecraft:zombie#main);
- native_draw_model_part: ModelPart.render under a mob's or block entity's
  frame, textured from an atlas sprite.

The humanoid mesh built with these builders bakes to exactly vanilla's
minecraft:zombie#main (an ignored test against the prepared resources).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 08:02:00 +04:00
Ludovic Marie a1710b6962 Merge pull request #10429 from horizon-rust/perf/seen-snapshot
🤖 Agent: the kept buffer is cleared at the end of every tick, so no villager Arc<villager::Seen> outlives the tick, and #[inline] on Mob::seen only changes where each Seen is written (in its slot instead of a stack copy): same values, same order. creature_prepare 1.2 → 0.8 ms in both A/B rounds.
2026-11-06 04:56:53 +01:00
voidsip e7956591f3 Match vanilla replacement decoding for packet strings 2026-11-05 21:53:19 -06:00
Horizon agentandClaude Sonnet 5.5 4b352e89c1 Options: the emote key tests read a versioned options.txt, rustfmt
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 04:52:55 +01:00
Horizon agent 2726d8040b Merge remote-tracking branch 'origin/main' into perf/seen-snapshot
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-06 04:52:00 +01:00
Horizon agentandClaude Sonnet 5.5 6da74ac228 Options: the emote key lives in horizon-extras.txt, options.txt is vanilla's again
Options parity regression from #10092: key.emote was in the vanilla key list, so options.txt
gained a key_key.emote line and every key after it shifted. It is now a Horizon-only mapping
(HORIZON_MAPPINGS): bound in Controls and translated like the others, saved as emoteKey in
horizon-extras.txt. A key_key.emote line written by the broken build is read once and dropped.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-06 04:44:03 +01:00
voidsip 6716cb7407 Merge remote-tracking branch 'refs/remotes/validation/worker7-main' into fix/4545-boat-paddle-transition
# Conflicts:
#	crates/horizon-client/src/game/remote_vehicles.rs
2026-11-05 21:43:23 -06:00
voidsip 8181ce0222 Reset mob fall distance before travel 2026-11-05 21:42:45 -06:00
Ludovic Marie 76b8b4eb83 Merge pull request #10423 from horizon-rust/fix/sculk-ticking-mobs
🤖 Agent: a mob that did not tick still gets its last position refreshed (no step), so one moved by a command while out of range doesn't count a walk or a landing once it ticks again; damage and mount events stay for every mob since vanilla raises them from hurt/startRiding. Proven on vanilla 26.3 with ore_probe2.py (settled pig/item stop lighting redstone ore once the player is 6 chunks away).
2026-11-06 04:40:22 +01:00
Florian ERNSTandCodex 6303017b03 Classify the verified hostile sound families as hostile
Co-Authored-By: Codex <[email protected]>
2026-11-06 04:38:19 +01:00
Horizon agent e16a5ef0da Merge remote-tracking branch 'origin/main' into fix/sculk-ticking-mobs 2026-11-06 04:37:28 +01:00
voidsip 76addba6cc Tick remote boat paddles and stop empty boats rowing 2026-11-05 21:36:53 -06:00
Skr1zzzz cc643f4af4 Random ticks: a section's candidate mask classifies each palette entry once
Rebuilding a changed or new section's random-tick candidate mask asked
whether the block state ticks randomly for each of its 4,096 blocks. A local
palette holds a handful of states: each entry is now classified once and the
mask built from the decoded palette indices. Single-value and global
palettes keep the previous path. Same mask, same draws.
2026-11-06 06:35:23 +03:00
TheMrZZandCodex gpt-6.1-sol c843c1aa05 Save the modeled mob loot-pickup flag
Write CanPickUpLoot on every save through the existing virtual getter, preserving the Allay and sulfur-cube overrides instead of retaining stale raw bytes. Cover decoded and mutated models plus frozen actual command reads.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 04:30:58 +01:00
Skr1zzzz 283c6723c0 Natural spawning: a start's height ends at the column's first empty block
NaturalSpawner.getRandomPosWithin draws Y in [minY, getHeight(WORLD_SURFACE)
+ 1], and ChunkAccess.getHeight is getFirstAvailable - 1: the top is the
first empty block. Horizon's world_surface already returns that block and
the spawner still added one, so every start could land one block higher
than vanilla's, in the air above the surface, where vanilla's draw never
goes; nextInt's bound was one larger too, so the draws differed from
vanilla's from there on.
2026-11-06 06:22:19 +03:00
Ludovic Marie 1532c9c153 Merge pull request #10331 from horizon-rust/perf/lan-entity-state-cache
🤖 Agent: idle entities (outside the simulation-level entity-ticking range) no longer run sendChanges, as in vanilla's ChunkMap.tick, so their 60-tick MoveEntityPos stops; a bare state (no data) is never paired, and the idle check follows the tracker's viewers (placed or held). tick_lan −1.66 ms (−24 %) in 3 A/B pairs.
2026-11-06 04:22:17 +01:00
voidsip 23a880843d Preserve custom model data flags through the item render bridge 2026-11-05 21:20:17 -06:00
voidsip 7b4634a912 Fix client steering for saddled pigs and striders 2026-11-05 21:20:10 -06:00
Ludovic Marie 2daa740748 Merge pull request #10412 from horizon-rust/fix/remote-player-offhand-nametag
🤖 Agent: queued. Every tracked entity now records what it rides, so any passenger (item/block display included) hides the player's tag as vanilla's isVehicle does; one risk checked: a removed vehicle leaves a stale id in passengers' vehicle, harmless since tags look up live entities.
2026-11-06 04:19:50 +01:00
Ludovic Marie cd547654d9 Merge pull request #10406 from horizon-rust/fix/aistep-ticking-mobs
🤖 Agent: the gate is the set of mobs the loop actually ticked (Creatures::ticked), not a second ticking-area test, so a mob that walks out of range during its own tick still finishes its pickups as in vanilla; proven against a vanilla 26.3 server (pickup_probe.py: only the ticking villager and zombie take their item).
2026-11-06 04:08:51 +01:00
Horizon agentandClaude Opus 5.5 0ed3868329 Name tags: any passenger hides its vehicle's name, as isVehicle does
Vanilla's LivingEntityRenderer.shouldShowName hides a name tag for any
entity with a passenger (`!entity.isVehicle()`). The client only counted
mobs, text displays, armor stands and interactions as passengers, so a
server that hangs its own name over a player with an item display, a
block display or any other entity still got the player's tag drawn
under it: two names (#5666).

Every tracked entity now records the vehicle SetPassengers puts it on,
and the name tags read that, whatever the passenger's type. The typed
lists stay for placing the riders.

The rest of #5666 was fixed already on main: the sword drawn in the
off hand by #5111 (each remote hand renders its own stack), the double
tag of custom-named players by #8457, and text display and armor stand
riders by #9754.

Fixes #5666

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:07:46 +01:00
SenjinandClaude Opus 5.5 7b80ab60c7 Shader packs: free the old buffers before a resize makes the new ones
A pack with large buffers (SEUS PTGI at 3440x1440) held both sets at once while resizing and ran
out of video memory going back to fullscreen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 04:00:58 +01:00
voidsip 6beb50ebcd Preserve boolean glint overrides in item model views 2026-11-05 20:58:38 -06:00
Horizon agent c1d16aabde Merge remote-tracking branch 'origin/main' into perf/seen-snapshot 2026-11-06 03:55:25 +01:00
Horizon agent c0c3096e85 Merge branch 'fix/aistep-ticking-mobs' into fix/sculk-ticking-mobs 2026-11-06 03:55:04 +01:00
Horizon agent 74150d5ef8 Merge remote-tracking branch 'origin/main' into fix/aistep-ticking-mobs 2026-11-06 03:54:43 +01:00
DUzzL dfb9757d97 Evoker fangs: honour scoreboard teams when deciding allies 2026-11-06 03:50:47 +01:00
Skr1zzzz a58d5700f3 Merge remote-tracking branch 'origin/main' into server-tick-spans
# Conflicts:
#	crates/horizon-client/src/game/creatures.rs
2026-11-06 05:50:36 +03:00
Skr1zzzz 9cdcf66e69 Spans: inside the server tick's mob spawning, block and creature steps
A slow tick named spawn_natural, tick_blocks and tick_creature_entities but
nothing inside them. With 100 bots they take ~11, ~21 and ~10 ms a tick, and
an index for spawn_selection's nearest-player shortlist saved nothing: the
time was elsewhere. Their steps now have laps (sn_*, tb_*, tc_*), and the
natural-spawn pass counts its attempts and spawns (sa_attempts/sa_spawns,
one note a tick). They show the attempt loop itself: ~18,850 attempts a tick
for ~3 spawns (#3910). Only when HORIZON_SPIKES is set.
2026-11-06 05:48:23 +03:00
Horizon agentandClaude Opus 5.5 c442edabd9 Merge origin/main; idle states follow the tracker's viewers (held players too)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:43:53 +01:00
Florian ERNSTandCodex 29f04e40ed Pass the tool to inline fishing loot
Co-Authored-By: Codex <[email protected]>
2026-11-06 03:42:10 +01:00
Horizon agentandClaude Opus 5.5 04f6bd1214 Sculk events: only mobs and items that tick step, land and flap
Steps, landings, splashes, flaps and stepping on redstone ore come from
an entity's own tick in vanilla (Entity.move, applyEffectsFromBlocks
from LivingEntity.aiStep and ItemEntity.tick), and ServerLevel.tick
ticks only the entities in entity-ticking chunks. Here every loaded mob
and dropped item on the ground stepped on the block under it each tick:
a pig or an item that had settled on redstone ore kept lighting it after
the player went six chunks away.

Those events are now for the mobs tick_creature_entities ticked (and
those standing in the ticking area, as the sculk fixture posts before
the mobs tick) and the items in the ticking area. A mob that did not
tick keeps where it is as its last position, without a step. Damage and
mounting are raised by hurt and startRiding, so they stay for every mob.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:29:26 +01:00
TheMrZZandCodex gpt-6.1-sol 27d093c94f Use registered XP-orb UUID in saved command reads
Reuse the existing identity getter rather than generating a UUID per read. Cover repeated saved reads, absent raw input and stale raw UUIDs through the actual Game getter and command fixture.

Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-06 03:21:06 +01:00
voidsip 36c0d3f938 Predict fishing bobber block hits before client movement 2026-11-05 20:20:54 -06:00
DUzzL 6bd507f89d Treat bubble column as liquid when validating a respawn point 2026-11-06 03:20:54 +01:00
masa-masa-0314andZCode 7a846eebc6 Realms: the settings tab's render and simulation distances
RealmsSettingsTab's two distance sliders, fed by
getRealmTierConfiguration (asked until it answers, as
tierConfigurationFetched does) and saved with updateRenderDistance /
updateSimulationDistance only when a value changed, alongside
saveSettings' existing name, description and region update.

Co-Authored-By: ZCode <[email protected]>
2026-11-06 10:17:04 +08:00
DUzzL bd152b1ecf Piglins: compare armour and weapons when both or neither item is wanted 2026-11-06 03:13:01 +01:00
Florian ERNSTandCodex 6f9981bfbf Use the baby pig passenger attachment when seating riders
Co-Authored-By: Codex <[email protected]>
2026-11-06 03:10:51 +01:00
voidsip 6cf1b6b74c Keep command tags when converting mobs 2026-11-05 20:10:33 -06:00
DUzzL 12454e54bd Port ResetUniversalAngerTargetGoal to iron golems 2026-11-06 03:08:44 +01:00
Horizon agentandClaude Opus 5.5 6b2c83656c Mob snapshot: build each Seen in its slot and keep the buffer
tick_creature_entities snapshots every loaded mob (`Seen`, ~400 bytes)
before the loop and again after each mob's tick. Mob::seen lives in the
client lib and was not inlined into the game crate, so each one was
built on the stack and copied into its slot, and the snapshot's
megabytes were allocated and freed every tick.

Mob::seen is #[inline] now, and the emptied buffer stays in Creatures
for the next tick. The snapshot holds the same values.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:07:34 +01:00
voidsip d7e1eaf955 Apply melee extra knockback to boats 2026-11-05 20:06:28 -06:00
CocofireHDandopencode 7c57f5f906 prepared files: treat a killed export as unused
A killed exporter left an empty gameplay-facts.json (or another table) behind.
The preparer only checked that the path exists, so "prepare again" kept the
empty file, and the launcher called the folder prepared and started the game
on it. Trust an earlier export only when it still parses, and treat a folder
with an empty top-level JSON table as not prepared so Play prepares it again.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 03:01:43 +01:00
Ludovic Marie 69fab597a6 Merge pull request #10203 from Detimes/feat-10174-mod-deps
🤖 Agent: Checked on the desktop against main 9f8f6842d (merges cleanly, lock passes --locked). All 19 new lock packages are permissive (MIT/Apache/BSD-3/ISC-style webpki/Zlib/BSL option on ryu); the only build scripts (mysql, mysql_common) just pass target env vars, no downloads; TLS is rustls+ring only, flate2 uses miniz_oxide, no openssl/native-tls/-sys/cmake. cargo tree for horizon-client (default, --no-default-features, --target all) is identical to main, so players' game and native-mod compile are unchanged; the launcher's cold cargo fetch gains 21 crates / +2.5 MB once (noise in wall time). Note for later: webpki 0.22 and rustls-pemfile are unmaintained upstream; worth moving to mysql 28 when it no longer rewrites the lock.
2026-11-06 02:45:58 +01:00
voidsip 790e5f14d4 docs: index design server and release guides 2026-11-05 19:35:16 -06:00
CocofireHDandopencode 3c01dce30d resources: name the JSON file a parse error came from
serde's "expected value at line 1 column 1" says nothing about which of the
prepared folder's tables is damaged, so a player with a killed export only saw
a crash report. Read the core tables through horizon_world::json_file, which
prefixes the path, and have the startup check name a top-level table left
empty before the window opens.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 02:28:51 +01:00
Horizon agentandClaude Opus 5.5 66dc3229a0 Mobs outside the simulation distance pick nothing up
Mob.aiStep's looting, the villagers' item sensor and the fox, dolphin,
panda, piglin, allay and sulfur cube item passes run after the mob loop
over every loaded mob. In vanilla they are part of the mob's own tick,
and ServerLevel.tick ticks only the entities in inEntityTickingRange: a
villager six chunks from the nearest player, loaded but outside the
simulation distance, took the bread at its feet here and not there.

The loop records the mobs whose tick ran (Creatures::ticked), and those
passes leave the others alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:20:47 +01:00
AxxioandAntigravity 2948fb86ed game/maps: honour can_place_on when using a map on a banner in adventure mode
In adventure mode, vanilla allows ItemStack.useOn if the item's can_place_on component matches the clicked block. Previously, use_map_on_banner checked only !self.may_build() and returned early without checking can_place_on.

This honours can_place_on_in_adventure on the banner's block position, allowing adventure mode players with matching maps to toggle banner markers.

Co-Authored-By: Antigravity <[email protected]>
2026-11-06 02:18:05 +01:00
Ludovic Marie 9f8f6842db Merge pull request #10366 from horizon-rust/perf/sculk-entity-events
🤖 Agent: each mob footstep scanned every mob three times (two id lookups and the warden/allay listener loop); now an event with no sculk block in reach and no warden or allay within 24 blocks skips the context and the listeners. Listeners give up past 16 blocks before any side effect, so vibrations are unchanged.
2026-11-06 02:16:56 +01:00
Ludovic Marie 1525ae0e3b Merge pull request #10364 from horizon-rust/fix/founder-claim-use-checkout
🤖 Agent: launcher-only fix. Maps the service's 403 use_checkout / not_offered_here to their own messages (86 tables), and drops one_click after use_checkout so the Shop opens the free checkout. Risk: none on the hot path; only the claim's error branch changes. Launcher tests 719 passed on the desktop.
2026-11-06 02:09:16 +01:00
Ludovic Marie 09450fb6f9 Merge pull request #10361 from horizon-rust/perf/mob-loop-overhead
🤖 Agent: villager_pickups tested every loaded villager against every dropped item, looking up tags before the reach: that was 12.8 of 23 ms of the mob phase in the load test (22.7 → 8.1 ms after this). The new ItemScan keeps the full scan's order and falls back to it if the item list changes under it.
2026-11-06 02:07:26 +01:00
Horizon agentandClaude Opus 5.5 53bc816fa1 Launcher: rustfmt the claim refusal test
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 02:03:19 +01:00
Ludovic Marie 2993ef0b20 Merge pull request #10116 from horizon-rust/pvp/legacy-knockback
🤖 Agent: merging. Every knock_back caller outside the legacy rules passes legacy = false, so the 26.3 path is unchanged. 1.8's air lift and the unhalved sprint push only run under the hosted-world switch.
2026-11-06 01:53:48 +01:00
Horizon agentandClaude Opus 5.5 1411281eea Launcher: a refused founder claim says why and leads to the checkout
The cosmetics service answers POST /v1/claim with 403 use_checkout where
Paddle sells (the set goes through its free checkout there) and 403
not_offered_here where the claim is left out. The launcher mapped neither,
so a player saw "Horizon's cosmetics service had a problem" (Discord bug
thread, 2026-11-05).

- Both codes are the Shop's own refusals now, each with its own words, in
  all 86 lang tables.
- After use_checkout the saved answer drops one_click, so the Wardrobe stops
  offering "Claim for free" and the Shop's button opens the free checkout on
  playhorizon.dev.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:52:42 +01:00
Ludovic Marie 12d7eaccdb Merge pull request #10115 from horizon-rust/pvp/legacy-server
🤖 Agent: merging. [combat] is only written when legacy is on, and unknown keys are rejected. The game reads it in dedicated.rs, and a bad section logs and keeps 26.3's rules. The test now looks for the section line, not the template's '# [combat]' comment, which made verify fail on the first head.
2026-11-06 01:51:17 +01:00
Horizon agent 92bc3bc40f Merge remote-tracking branch 'origin/main' into perf/sculk-entity-events 2026-11-06 01:50:16 +01:00
Horizon agent d6d7cc3e29 Merge remote-tracking branch 'origin/main' into perf/mob-loop-overhead 2026-11-06 01:49:56 +01:00
Ludovic Marie 342824b506 Merge pull request #10114 from horizon-rust/pvp/legacy-rules
🤖 Agent: merging. legacy_rules() is remote.is_none() && combatRules, so a window on any server keeps 26.3's cooldown and damage. Measured over LAN: 1.8 rules 7 and 7 (diamond axe, charged then right after a swap), 26.3 9 and 3.6.
2026-11-06 01:48:43 +01:00
Ludovic Marie 195515000a Merge pull request #10112 from horizon-rust/pvp/legacy-effects
🤖 Agent: merging. The 1.9 hit sounds and particles are filtered on receipt (remote.rs, lan_particles.rs), so nothing sent changes. Hurt sounds and crit particles stay, and 1.8 hit sounds are off by default.
2026-11-06 01:46:16 +01:00
Ludovic Marie 1ff952a4f0 Merge pull request #10111 from horizon-rust/pvp/legacy-hand
🤖 Agent: merging. Legacy only moves the hand's matrices, and only for items with vanilla's flat display (pack-moved and post-1.8 items keep 26.3's). It's off by default, and the renderer is the only reader of the equip state it changes. On scripted fights against a vanilla server, #10117's Modern-vs-Legacy packet comparison matched on every scenario that is stable between two identical clients.
2026-11-06 01:43:50 +01:00
AxxioandAntigravity b08f1074b1 docs: remove stale "not ported" notes
Co-Authored-By: Antigravity <[email protected]>
2026-11-06 01:28:10 +01:00
AxxioandAntigravity 9a076b6a24 docs/README.md: index missing player, launcher and platform pages
Co-Authored-By: Antigravity <[email protected]>
2026-11-06 01:26:57 +01:00
Horizon agentandClaude Opus 5.5 b40c996fb9 Mob looting: walk only the items in the cells a reach meets
A mob near some dropped item tested every item on the level (2,000 in
the load test: 1.5 ms a tick). ItemAreas now keeps the items of each
cell, and ItemScan walks the list in its order from one item in a met
cell to the next, testing what the scan tested; it keeps up with the
items the walk removes, and tests every item from there on if the list
changes any other way. The villagers' pickups walk the same way.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:26:11 +01:00
Horizon agent 99e1caf4db Merge remote-tracking branch 'origin/main' into perf/sculk-entity-events 2026-11-06 01:22:32 +01:00
Horizon agent 400335608e Merge remote-tracking branch 'origin/main' into perf/mob-loop-overhead 2026-11-06 01:22:31 +01:00
Ludovic Marie e5875b0b19 Merge pull request #10342 from horizon-rust/agent/native-call-attribution
🤖 Agent: launcher-only fix in native mod error tracing; fmt/clippy/613 lib tests green on the desktop, new test fails without the fix.
2026-11-06 01:18:17 +01:00
Horizon agentandClaude Opus 5.5 1ab39a15ec Native mods: an error in the call horizon-mixins generates for a helper names the helper's mod
The launcher traced a compile error to a mod only inside the helper function itself. horizon-mixins
wraps each helper in a block that calls it, `{ fn helper(…) { … } helper(…) }`. When Horizon
changes a type the helper's signature mentions (flair 1.0.0: `extra` became five-element tuples),
the error lands on that generated call, so no mod was named and the whole build failed instead of
leaving that mod out. A block holding only the helper and its call now counts as the helper's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:16:25 +01:00
CocofireHDandopencode 89ca6821b8 test: keep docs/README.md links and benchmark index resolving
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:10:13 +01:00
CocofireHDandopencode 4eafef1180 docs: index the benchmark and performance result pages
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-06 01:10:11 +01:00
Horizon agentandClaude Opus 5.5 e82ac45192 Format the idle-entity change
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 01:04:26 +01:00
Ludovic Marie 764df32826 Merge pull request #9987 from horizon-rust/mods/hotload-design
🤖 Agent: merging the mod hot-load design note, docs only. The point to check is the native measurement it rests on: a one-line gameplay change rebuilds in 570 s against 812 s cold, and mold doesn't help, which is why the run-time switch came before background builds.
2026-11-06 00:53:50 +01:00
Ludovic Marie 75e54413d7 Merge pull request #10217 from horizon-rust/mods/hotload-content
🤖 Agent: merging live switching of content mods. The point to check is the server's world tables: with no data pack they used to stay on the startup resources, so a switched mod's data never applied. After a live switch they are now built from the new root (data_switched), checked in game by a mod function that loads, unloads and loads again.
2026-11-06 00:53:47 +01:00
Ludovic Marie e0bd7cd69a Merge pull request #8742 from Akirabane/feat/7484-request-logs
🤖 Agent: the request log reads the player's name before the request runs, so a release or drop that removes the guest still logs who it was. The e2e this PR adds ran on this exact stack against current main: 31/31 over real control sockets with horizon-bots.
2026-11-06 00:47:45 +01:00
Ludovic Marie be04fbb531 Merge pull request #8735 from Akirabane/feat/7484-import-position
🤖 Agent: pushed 4020e2537 for martipops' review. It refuses a rotation that overflows f32 (1e39 became inf/NaN after the cast), refuses a named position when the record still has a RootVehicle to restore (the vehicle would undo it), keeps recentered on retries, and documents that velocity is the server's body only. Tests added for each; union-tested on the desktop.
2026-11-06 00:47:41 +01:00
Ludovic Marie 745956e29b Merge pull request #8730 from Akirabane/feat/7484-import
🤖 Agent: the commit point holds. place_arrival writes the .dat with write_dat_durable plus the advancements and stats with write_durable, each a Result, before ok, and the player stays held on any error. Retries after the commit answer through placed_arrival. martipops' two reviews are answered point by point in the comment above. server_transfer_e2e passed on the final stack (31/31).
2026-11-06 00:47:37 +01:00
Ludovic Marie 7b36dd82a3 Merge pull request #8716 from Akirabane/feat/7484-held-player
🤖 Agent: with [transfer] off, no login is expected, so held stays false and watches() equals here(): every here→watches swap in hub.rs and the loading_chunks refactor are no-ops for ordinary servers. With it on, a held player is a viewer but not tracked, and gets list changes through broadcast_watching. Union-tested with main and the stack on the desktop, plus server_transfer_e2e (31/31).
2026-11-06 00:47:32 +01:00
Horizon agentandClaude Opus 5.5 153044c095 Sculk: a mob's game event costs nothing when nothing can hear it
tick_mob_sculk_events posts every loaded mob's footsteps, landings and
splashes. Each event looked its mob up by a scan of every mob (for the
context and the supporting block) and asked every mob whether it was a
warden or an allay, so a server with thousands of mobs paid three scans
of them for each footstep near no listener.

Now, as GameEventDispatcher only visits the listeners around the event:
while the mobs' events are posted, mobs are found by id through a slot
map, the wardens and allays are listed once with their positions, and
an event with no sculk block in reach and no warden or allay within 24
blocks skips the context and the listeners. A mob that stands as it
stood with no step due (every mob outside the simulation distance)
leaves at once, and a mob riding nothing, as before, skips the mount
bookkeeping. Mods still get every event (native_game_event).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:43:01 +01:00
Horizon agentandClaude Opus 5.5 f43950f5fb Design note: the measured times, and what PRs 2 to 4 did
Native rebuild times measured on the desktop, live switch times in game
on the ThinkPad and the content root on the desktop. Option A as built:
trait mods switch at run time, RenderHooks and the launcher's build-all
come next. The browser's buttons and index fetch as implemented.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:38:39 +01:00
Horizon agentandClaude Opus 5.5 b0aacebaab Mods hot load: a test follows the root-per-mod-set rule
blocks_and_items_declared_in_code_get_ids expected a changed declaration
to rewrite the same root's stamp; each set now has its own root, so it
checks the new root and that the old one is left as it was. Found by
running the content-root tests on real prepared resources.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:37:43 +01:00
Horizon agentandClaude Opus 5.5 1d8a1699ad Mob loop: villagers test reach before item tags, no player view copies
villager_pickups tested every loaded villager against every dropped item,
looking the item up in the villager food table and tags before the reach:
on a server with hundreds of villagers and thousands of items that was
most of the mob phase. The reach comes first now, and a villager with no
item in its cells skips the scan (ItemAreas, as the looting passes do).

Mobs outside the simulation distance only check despawning, which reads
the nearest player and the difficulty: they no longer copy the player
view they see. A ticking mob borrows its guest's view for its tick (the
tick reads it through &MobWorld) instead of cloning it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:30:30 +01:00
Ludovic Marie 42e6d9cc49 Merge pull request #10263 from foudretbr/fix/unknown-block-state-panic
🤖 Agent: unknown states now read as air at the three places they enter (chunk palettes, block updates, BlockFacts::get), and the palette scan is skipped when nothing is unknown, so a vanilla-width chunk costs one maybe_has over its palette. Vanilla would disconnect on such an id (MissingPaletteEntryException) instead; air matches what Horizon already did for a wider host, so I took it. Union-tested on the desktop (clippy, wide_states, horizon-items).
2026-11-06 00:29:30 +01:00
Ludovic Marie 369e466faa Merge pull request #10080 from foudretbr/prepare-ignore-global-java-options
🤖 Agent: removing JAVA_TOOL_OPTIONS, _JAVA_OPTIONS and JDK_JAVA_OPTIONS in java::command reaches every exporter JVM and javac but not the game's own launch, so a player's global -XX:+UseZGC still applies in play and only stops clashing with BATCH_OPTIONS' SerialGC during preparation. Built and tested in a union on the desktop (clippy, horizon-prepare tests).
2026-11-06 00:29:25 +01:00
Horizon agent 4f3355c151 Merge remote-tracking branch 'origin/main' into perf/lan-entity-state-cache 2026-11-06 00:26:38 +01:00
Horizon agentandClaude Opus 5.5 34ddea78e1 Merge #8735's review fixes into #8742
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:22:26 +01:00
Horizon agentandClaude Opus 5.5 4020e25379 player-import: refuse an overflowing rotation and a named position over a vehicle
Answers martipops' review of #8735:
- A rotation finite as f64 but past f32::MAX became an infinity (and a NaN pitch) after the
  cast; it is refused now, like any rotation that is not a number.
- A record with a vehicle still to restore (RootVehicle) seats the player back on it at the
  vehicle's own place, which undid a position the request named; the two are refused together.
- A retried import of the same hand-over, or a retry after the commit, answers the same
  `recentered` as the first reply (kept on the guest until it is placed).
- docs/server.md says the velocity is the server's only: the arrival's teleport carries no
  motion, as a vanilla login's does not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:22:22 +01:00
Horizon agentandClaude Opus 5.5 7dcc5b46f3 Server: send idle entities' changes as ChunkMap does, describe them bare (#10253)
ChunkMap.tick runs ServerEntity.sendChanges only for an entity that changed
section, needs sync, or is in DistanceManager.inEntityTickingRange. Horizon
ran it for every tracked entity, so an idle one sent its position every 60
ticks and advanced its update clock. EntityState::idle now carries the
range (the simulation level, Levels::in_entity_ticking_range), and the
tracker skips sendChanges for an idle entity that stayed in its section and
needs no sync; a lead change still goes out at once (Leashable).

The game then stops describing such entities in full every tick: an idle
mob or item that was in the tracker's last pass at the same place, carries
no passengers, and is already tracked by every client within its tracking
range is described bare (where it is, none of its data). The tracker never
reads its data and never pairs a bare state, so every pairing still sends
the entity as it is (game/lan_idle.rs).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:21:37 +01:00
Ludovic Marie 2b74332fbe Merge pull request #10117 from horizon-rust/pvp/wire-stream-legacy
🤖 Agent: merging. The default --clients vanilla,horizon run is unchanged, and so is main's one-client capture (now 'one or two of'). horizon-legacy only adds a horizon-extras.txt in its own scratch home. On the desktop, Modern vs Legacy matched on 14 of 16 scenarios; eat and dig_teleport also differed between two identical Modern clients there.
2026-11-06 00:13:00 +01:00
Ludovic Marie f563b7aafe Merge pull request #10308 from horizon-rust/fix/launcher-pictures-gpu-limit
🤖 Agent: merging. Pictures are now sized to the window × scale and capped by the atlas limit (the wgpu adapters' min on Linux, 16384 on Metal/DX), instead of a fixed 3840. RSS stays flat at 173 MiB for 5 min on a real GPU. One thing to watch: the Linux probe opens its own Vulkan/GL instance at startup on a background thread, and until it answers pictures cap at 8192.
2026-11-06 00:12:56 +01:00
Horizon agentandClaude Opus 5.5 f8c3edd81e launcher: make pictures at the drawn size, capped by the GPU's limit
#10236 capped screenshots and the background at a fixed 3840 px, so an
8K screen lost detail. Pictures are now made at what the screen shows:

- viewer and background: the window's longer side x its scale factor
  (7680 px on an 8K screen), tiles 360 points x the scale factor, in
  256 px steps so a resize doesn't remake them every frame;
- never above the biggest texture GPUI's atlas holds: on Linux the
  smallest max_texture_dimension_2d over wgpu's Vulkan/GL adapters
  (asked once in the background, 8192 until known), elsewhere the fixed
  16384 of the Metal and DirectX atlases. A larger picture would make
  the atlas add a max-size texture on every frame (zed#66595).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:10:21 +01:00
Horizon agentandClaude Opus 5.5 5ba18cae86 PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 f06b417ac4 Server: the [combat] test looks for the section, not the template's comment
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 346a0a8d81 lan_e2e: legacy-pvp hits right after a hotbar swap for an uncharged hit
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 fc8781c2ce PvP & HUD: document Legacy's sounds and particles
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 6d36365aeb PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 3400a0316b PvP & HUD: 1.8 knockback under legacy combat rules
With the legacy rules on, a player's knockback follows 1.8.9's
EntityLivingBase.knockBack: the motion halves and the 0.4 push lifts in the
air too (26.3 lifts only from the ground), and knockback resistance is a
chance to take none instead of a cut. The sprint and Knockback push is added
on top as attackTargetEntityWithCurrentItem's addVelocity (0.1 up) instead
of going through knockback, for players and mobs alike.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 4d4c3ff32f Server: [combat] legacy = true in server.toml for 1.8 combat rules
horizon-server accepts and keeps a [combat] section (off by default, left out
of the file while off); the dedicated game reads it itself, like
[anticheat], and turns on the legacy rules a hosted world's PvP page switch
gives. A wrong section is logged and fights keep 26.3's rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 932050ffab lan_e2e: legacy-pvp scrolls the PvP page to its switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 04c3b710cf PvP & HUD: Legacy leaves out the sweep, damage hearts and 1.9 hit sounds
Under Combat feel Legacy the client draws no sweep_attack or
damage_indicator particles a server sends (1.8.9 had neither), and with
'1.8 hit sounds' plays none of the entity.player.attack.* sounds 1.9
added. Hurt sounds, crits and enchanted hits stay. Only what this client
shows changes; nothing is sent.

1.8.9 drew no red tint over armour (LayerArmorBase.shouldCombineTextures
is false, so setBrightness skips it) and the body's flash is red at 0.3,
as 26.3's: both already match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 a6f2414081 PvP & HUD: document Combat feel
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 62db23c23b PvP & HUD: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 eabc4160a8 PvP & HUD: Combat feel Legacy draws the hand as 1.8.9 did
Combat feel: Modern (default) or Legacy (1.8.9). Legacy changes only the
picture: a flat item in first person takes 1.8.9's ItemRenderer pose
(swing, eating and drinking, bow pull) through 26.3's own display
transform, a sword is held up while use is held (1.8.9's block pose,
swinging as 1.7.10 did), the main hand no longer dips after each hit,
and the attack indicator hides unless the player keeps it. The clicks
and every packet stay 26.3's. Poses 1.8.9 lacked (crossbow, trident,
spear, brush) and resource packs' own displays stay modern.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 cccc296d9a PvP & HUD: the fall distance is already an f64
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agentandClaude Opus 5.5 270e435b3f PvP & HUD: 1.8 combat rules in worlds you host
A PvP page switch, off by default: single player and LAN worlds resolve
melee hits by 1.8.9's rules (EntityPlayer.attackTargetEntityWithCurrentItem):
no attack cooldown and no sweep, 1.8.9 weapon damage (ItemSword 4 / ItemAxe 3 /
ItemPickaxe 2 / ItemSpade 1 plus ToolMaterial), Strength x(1 + 1.3 per level)
and Weakness -0.5 per level, Sharpness 1.25 per level, and critical hits that
allow sprinting and need no charge. Only a game that is the server uses them;
a window playing on a server never does, so what it sends stays the same.

The window passes the switch to its server thread each frame, like the
distances. lan_e2e's legacy-pvp check measures two diamond axe hits 12 ticks
apart: 7 and 7 under the rules, 9 then a cooldown-cut hit without.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 00:01:54 +01:00
Horizon agent 5f625c748e Merge remote-tracking branch 'origin/main' into pvp/wire-stream-legacy
# Conflicts:
#	tools/test_wire_stream_parity.py
2026-11-06 00:00:26 +01:00
Ludovic Marie 2266c4de14 Merge pull request #10211 from horizon-rust/pvp/hud-editor-main
🤖 Agent: merging. These are #10079's commits merged with main and nothing else; #10079 had landed in the already-merged pvp/hud-layout. The editor opens only from a world and is the only screen that writes the layout.
2026-11-06 00:00:04 +01:00
Ludovic Marie 76c0362eb8 Merge pull request #10225 from horizon-rust/agent/native-broken-on
🤖 Agent: verified on the desktop: horizon-mods 36 tests, launcher 589 tests, clippy -D warnings clean. broken_on is read only from the registry index (the publish job adds it from horizon-mods' daily native-compat run). The warning shows only when the entry's Horizon is the commit the mod would build on, so a mod pinned elsewhere doesn't warn.
2026-11-05 23:57:29 +01:00
Horizon agent 12c5d55a95 Merge commit '765da7031e28448d840a218a2b87686d8fb6d59d' into agent/native-broken-on 2026-11-05 23:51:42 +01:00
Ludovic Marie 765da7031e Merge pull request #10082 from horizon-rust/pvp-safe/wire-stream-harness
🤖 Agent: the harness itself: 16 scenarios, real vanilla 26.3 client vs Horizon, per tick and field; on main 11/16 match, with the series + #9918 16/16. Named-only and solo in parity_hosts (needs a virtual screen, ~15 min). Merge of main resolved a game.rs mod-list conflict (tick_profile + tick_script).
2026-11-05 23:50:44 +01:00
Ludovic Marie 89e625981b Merge pull request #10084 from horizon-rust/pvp-safe/held-slot-tick
🤖 Agent: wire stream harness (#10082) on main: number keys' SetCarriedItem left a tick early (hotbar_swap, 15 ticks; dig_teleport) and a key tapped with a swing at the sky sent the slot before the Punch; with this change every tick matches vanilla. Head = cc7200cd7a's +35 −43 plus merges of #10083, #10085 and main (conflicts in remote.rs resolved to the tree the harness after-run built and passed).
2026-11-05 23:50:40 +01:00
Ludovic Marie 1f09d99dad Merge pull request #10215 from horizon-rust/agent/native-build-health
🤖 Agent: verified end to end on the desktop: with 30 registry mods on Nightly 2026-11-05, main's launcher fails the whole build (eatinganimation E_TARGET_NOT_FOUND), and this one builds 22 and leaves out 8 with their reasons. MIXINS_REV moves to 5a64af9 (horizon-mixins#27, #28). The risk to watch: a compile failure costs one full recompile per round, capped at 3.
2026-11-05 23:50:36 +01:00
Horizon agentandClaude Opus 5.5 fe49561c78 Merge main into held-slot-tick after #10083 and #10085 landed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:48:21 +01:00
Horizon agent 152eaed3bd Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 23:36:37 +01:00
Horizon agent efbd46aaad Merge commit '6d99e14ff0c70ae60bc56b67829b526409fa068e' into agent/native-build-health
# Conflicts:
#	launcher/lang/af.json
#	launcher/lang/ar.json
#	launcher/lang/az.json
#	launcher/lang/ba.json
#	launcher/lang/be.json
#	launcher/lang/bg.json
#	launcher/lang/br.json
#	launcher/lang/bs.json
#	launcher/lang/ca.json
#	launcher/lang/cs.json
#	launcher/lang/cv.json
#	launcher/lang/cy.json
#	launcher/lang/da.json
#	launcher/lang/de.json
#	launcher/lang/el.json
#	launcher/lang/eo.json
#	launcher/lang/es.json
#	launcher/lang/et.json
#	launcher/lang/eu.json
#	launcher/lang/fa.json
#	launcher/lang/fi.json
#	launcher/lang/fil.json
#	launcher/lang/fo.json
#	launcher/lang/fr.json
#	launcher/lang/fur.json
#	launcher/lang/fy.json
#	launcher/lang/ga.json
#	launcher/lang/gd.json
#	launcher/lang/gl.json
#	launcher/lang/haw.json
#	launcher/lang/he.json
#	launcher/lang/hi.json
#	launcher/lang/hr.json
#	launcher/lang/hu.json
#	launcher/lang/hy.json
#	launcher/lang/id.json
#	launcher/lang/ig.json
#	launcher/lang/is.json
#	launcher/lang/it.json
#	launcher/lang/ja.json
#	launcher/lang/ka.json
#	launcher/lang/kk.json
#	launcher/lang/kn.json
#	launcher/lang/ko.json
#	launcher/lang/ky.json
#	launcher/lang/la.json
#	launcher/lang/lb.json
#	launcher/lang/li.json
#	launcher/lang/lmo.json
#	launcher/lang/lo.json
#	launcher/lang/lt.json
#	launcher/lang/lv.json
#	launcher/lang/mk.json
#	launcher/lang/mn.json
#	launcher/lang/ms.json
#	launcher/lang/mt.json
#	launcher/lang/nl.json
#	launcher/lang/no.json
#	launcher/lang/oc.json
#	launcher/lang/pl.json
#	launcher/lang/pt-br.json
#	launcher/lang/pt.json
#	launcher/lang/ro.json
#	launcher/lang/ru.json
#	launcher/lang/sah.json
#	launcher/lang/se.json
#	launcher/lang/sk.json
#	launcher/lang/sl.json
#	launcher/lang/so.json
#	launcher/lang/sq.json
#	launcher/lang/sr.json
#	launcher/lang/sv.json
#	launcher/lang/szl.json
#	launcher/lang/ta.json
#	launcher/lang/th.json
#	launcher/lang/tr.json
#	launcher/lang/tt.json
#	launcher/lang/uk.json
#	launcher/lang/ur.json
#	launcher/lang/uz.json
#	launcher/lang/vec.json
#	launcher/lang/vi.json
#	launcher/lang/yi.json
#	launcher/lang/yo.json
#	launcher/lang/zh.json
#	launcher/lang/zu.json
2026-11-05 23:36:06 +01:00
Ludovic Marie 6d99e14ff0 Merge pull request #10189 from horizon-rust/pvp-safe/click-slot-order
🤖 Agent: inventory in the wire stream harness: a shift-click's changed slots went out 10, 37 where vanilla's Int2ObjectOpenHashMap iterates 37, 10; now byte-identical, unit test pinned to the captured order.
2026-11-05 23:30:06 +01:00
Ludovic Marie 6421a384f5 Merge pull request #10188 from horizon-rust/pvp-safe/pick-view-vector
🤖 Agent: bridge in the wire stream harness: 19 of 19 UseItemOn cursors differed from vanilla by ~6e-5 on main (double trig in the pick ray), all bit-identical with Mth's float table. Hot path cost: one table lookup per frame instead of libm sin/cos.
2026-11-05 23:30:02 +01:00
Ludovic Marie 39e99fdbd6 Merge pull request #10085 from horizon-rust/pvp-safe/vehicle-order
🤖 Agent: read against LocalPlayer.sendChanges and AbstractBoat.tick (rider look, vehicle, then sprint; paddles first). No riding scenario in the harness yet, so the proof is the read and the unchanged 16 scenarios. Land before #10084, which merges this branch.
2026-11-05 23:29:58 +01:00
Ludovic Marie ad1a6a3101 Merge pull request #10083 from horizon-rust/pvp-safe/dig-sequence
🤖 Agent: verified with the wire stream harness (#10082): on main, dig_teleport answered a teleport mid-dig with a new START (sequence 34) instead of vanilla's ABORT; with the series every tick matches. pr_ready clean except the desktop nix-shell's uname-less install.sh test.
2026-11-05 23:29:54 +01:00
Horizon agentandClaude Opus 5.5 e692430ffe Merge #10085 (pvp-safe/vehicle-order) into held-slot-tick: both rewrite remote_send_state
The held slot leaves remote_send_state (this PR); the boat's paddles go first and a
passenger sends its look, vehicle, then sprint (#10085).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:26:29 +01:00
Horizon agentandClaude Opus 5.5 861d3168cb Merge #10083 (pvp-safe/dig-sequence) into held-slot-tick: both edit the login's reset
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:26:03 +01:00
Horizon agentandClaude Opus 5.5 3431d0e18f wire stream: a one-client run (--clients horizon) stops after its capture
A loaded desktop's vanilla client failed to log in; rerunning Horizon alone and
comparing with the earlier vanilla capture crashed on the missing client.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:23:56 +01:00
Ludovic Marie c4e4800d44 Merge pull request #10241 from martipops/controller/buttons-prompts-v2
🤖 Agent: the keyboard path is a mechanical move from keys.contains to held(Action), with the same default keys underneath; old horizon-controller.txt files keep their buttons (no bindLayout line), and the pad's stack drop also reaches the server path through drop_stack. Union pr_ready green.
2026-11-05 23:19:40 +01:00
Ludovic Marie f33f49aa06 Merge pull request #10181 from martipops/fix/split-pack-overlay
🤖 Agent: a guest now gets host.vanilla_root(), not P1's overlay, so build_overlay_uncached can no longer delete the files it is building from.
2026-11-05 23:19:36 +01:00
Ludovic Marie c4c815689b Merge pull request #10059 from martipops/fix/pad-e2e-linux
🤖 Agent: harness only; hard links keep 60000 regions cheap on disk, and focus_on no longer raises StopIteration on a scrolled-out row.
2026-11-05 23:19:32 +01:00
Ludovic Marie 2aaaa76e9b Merge pull request #10056 from martipops/feat/pad-status-mapping
🤖 Agent: the router's per-frame status list is built only while a control socket is open, so splitscreen play without horizon-ctl pays nothing for it.
2026-11-05 23:19:28 +01:00
Ludovic Marie 460dbb0e5e Merge pull request #10054 from martipops/fix/ctl-socket-path
🤖 Agent: the check is SocketAddr::from_pathname, and the test compares it with a real bind at every length 1-150, so it follows the OS limit (104 on macOS, 108 on Linux) without a hardcoded number.
2026-11-05 23:19:24 +01:00
Ludovic Marie b756cf01e6 Merge pull request #10029 from foudretbr/feat/launcher-socks-proxy
🤖 Agent: SOCKS goes through ureq's own socks-proxy feature (socks 0.3.4, MIT/Apache), and the loopback SOCKS5 test proves the request reaches an .invalid host only through the proxy. Nothing changes for players without a proxy set.
2026-11-05 23:19:20 +01:00
Ludovic Marie d6bac327fa Merge pull request #9966 from Mayonesu0/fix/9633-wheel-scrolls-settings-list
🤖 Agent: matches 26.3: OptionsList inherits AbstractScrollArea.mouseScrolled, so the wheel scrolls the list and never reaches a row's CycleButton; the old key is dropped because every install saved it as true. Main merged in, union pr_ready green.
2026-11-05 23:19:15 +01:00
Horizon agent a35ea8b72a Merge remote-tracking branch 'origin/pvp-safe/wire-stream-harness' into pvp/wire-stream-legacy 2026-11-05 23:11:05 +01:00
Horizon agent ec21e06600 Merge remote-tracking branch 'origin/main' into pvp-safe/wire-stream-harness
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-11-05 23:08:51 +01:00
Horizon agentandClaude Opus 5.5 9027fcd390 Time tick_creature_entities' steps as spans for horizon-report
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:07:47 +01:00
Horizon agentandClaude Opus 5.5 8d09d8b3f2 wire stream: a teleport's two answer ticks compare with each other, the rest on the script's clock
Aligning every tick after the teleport on its acceptance moved the script's own
packets (the button release, the 20-tick position) by the server's lag: on the
desktop vanilla got the answer at tick 26, Horizon at 29, and both clients'
packets were otherwise identical tick for tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:06:50 +01:00
Horizon agentandClaude Opus 5.5 25d55ff457 Merge main into mods/hotload-content (after #10104 and #10218)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:06:43 +01:00
Ludovic Marie 4f4a931b21 Merge pull request #10269 from horizon-rust/agent/redstone-catalog-harness
🤖 Agent: harness and examples-only probe, nothing players run. New head: CI's parallel shards raced on one schematic .part file; each process now writes its own (three concurrent downloads checked against the pinned digest), and parity / parity passes. Point to check: item-moving builds are reported as not comparable, never as a pass. Result: 111/111 verified contraptions match vanilla.
2026-11-05 23:05:43 +01:00
Horizon agent cabfc96e14 Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 23:00:38 +01:00
Horizon agentandClaude Opus 5.5 2044fc756c launcher: trace a compile error inside a mod's support module to that mod
The second end-to-end build of 30 registry mods on 6475f62 stopped on
errors inside carryblock's, distant_horizons', smartf3's and treefall's
support modules (made before the crate split), which named no mod: only
injected helpers were traced. An error inside the inline module a mod
declared (one the pristine source doesn't have) now names that mod, so
the build leaves all of them out in one more compile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 23:00:08 +01:00
Horizon agentandClaude Opus 5.5 043705f347 Redstone catalogue: each shard writes its own schematic part file
CI runs the three shards at once on an empty .local/lra-catalog: two of them
downloaded the same schematic into one `.part` name and one rename found it
gone. The part file now carries the process id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:58:28 +01:00
Ludovic Marie 0f5dc8d6d5 Merge pull request #10268 from horizon-rust/agent/redstone-catalog-oracle
🤖 Agent: test-only oracle (mods/horizon/src/test). Point to check: run() clears block ticks after it loads containers and comparators, and sets the lectern's page field directly. Otherwise setChanged and setPage schedule ticks or power the lectern before step 0, which is what made 29 builds differ by an extra tick (see #10269's numbers).
2026-11-05 22:55:22 +01:00
009378de29 Stop the underwater sky scan on kelp and seagrass.
canSeeSkyFromBelowWater stops when a block dampens light and is not liquid(). Kelp and seagrass hold water, so guardians under them were still rolling the 1-in-20 and spawned less often.

Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-11-05 22:53:40 +01:00
b9607ce998 Replace a bubble column when a bucket empties into it.
emptyContents destroys the block only when it is not liquid(). A bubble column is liquid and not a LiquidBlock, so breaking it first played destroy particles and a sculk vibration.

Co-Authored-By: Grok <grok-4.7> <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-11-05 22:53:40 +01:00
Ludovic Marie 83da211d38 Merge pull request #10267 from horizon-rust/agent/redstone-catalog-data
🤖 Agent: data and reader only, nothing players run. The catalogue is WTFPL from maybeachromium (LICENSE beside it). Point to check: tools/lra_schem.py is the one parser of untrusted downloaded schematics. It reads gzip NBT with struct only, never executes or imports from them, and rejects a palette/varint count mismatch.
2026-11-05 22:48:26 +01:00
Horizon agentandClaude Opus 5.5 b7df353fdf Merge main into fix/9633-wheel-scrolls-settings-list
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:42:57 +01:00
foudretbr d55c2840a7 Block states: map states past registry to air instead of panicking
A server with mods or states the client does not have can send block states outside our registry. Instead of panicking with an out of bounds slice access when indexing BlockFacts or unpacking palettes, map unknown states to air.
2026-11-05 22:34:51 +01:00
DUzzL cb7babf1cf Normalize END records whose disk numbers are equal but not zero 2026-11-05 22:32:18 +01:00
Ludovic Marie 3c7bb44aa1 Merge pull request #10220 from xxaxio/fix/10072-launcher-auto-update-race
🤖 Agent: per-home mod lock fixes the stale-manifest race; UI-thread switch/remove use try_lock_home so an update's downloads never freeze the window.
2026-11-05 22:31:06 +01:00
Ludovic Marie 51e5d01c83 Merge pull request #10145 from horizon-rust/cosmetics-fx/wardrobe
🤖 Agent: Wardrobe layer, retargeted to main after #10144. preview::Draw gains alpha and Draw::model; main's live view now builds the body with it. A companion is drawn at its anchor, an aura see-through at the feet with its turned-away faces skipped, and a trail as a short run of particles. The Wardrobe gets Companion, Trail and Aura rows, in all 86 launcher languages. Verified on this head: rustfmt on game and launcher, 71 horizon-cosmetics tests, launcher wardrobe/i18n/lang tests (23 + 7).
2026-11-05 22:29:16 +01:00
Ludovic Marie b0a738abc3 Merge pull request #10144 from horizon-rust/cosmetics-fx/render
🤖 Agent: render layer, retargeted to main after #10143. It draws companions on the GPU skin path (identical ones share one buffer) and trails and auras in one see-through span after the players. Nothing is drawn beyond 48 blocks or outside the view. Measured in-game with HORIZON_COSMETICS_LOCAL: +0.6 ms prepare at 16 wearers and +1.1 to 2.4 ms at 64, mostly the auras built on the CPU; the follow-up is #10251. Verified on this head: horizon-client clippy -D warnings.
2026-11-05 22:26:58 +01:00
Ludovic Marie c8eee9842f Merge pull request #10143 from horizon-rust/cosmetics-fx/settings
🤖 Agent: settings layer, retargeted to main after #10142. The unused "Show other players' auras and trails" (off) becomes "Hide other players' trails and auras", off by default; it is the cosmeticsHideFx line, shared by the game's Extras and the launcher's Wardrobe, and the old cosmeticsAuras line is dropped on save. Translated into the game's 75 languages and the launcher's 86. Verified on this head: clippy -D warnings on game and launcher.
2026-11-05 22:24:25 +01:00
Ludovic Marie 731544d1c5 Merge pull request #10218 from horizon-rust/mods/native-switch
🤖 Agent: merging the native on/off switch. The point to check is the hot path: each Hooks dispatch adds one relaxed atomic load per mod, and lists that aren't a slice of the global MODS (tests, single-mod dispatch) are never gated. RenderHooks and mixin code are left unswitched on purpose, as the PR says.
2026-11-05 22:24:20 +01:00
Ludovic Marie a27b7f20fe Merge pull request #10213 from horizon-rust/agent/smp-profiler
🤖 Agent: horizon-report format 2 (mob costs, level spans, tick_lan/sculk sub-spans, workers, network, overhead, stop/status). verify green on eb55d13; live on the SMP since 21:08Z as prof2 at 0.03 % of MSPT; the one desktop test failure is a load-sensitive 1 s login cap that passed 3/3 alone. Only spans and counters added, no gameplay change.
2026-11-05 22:24:16 +01:00
Ludovic Marie 2ed3ede17f Merge pull request #10142 from horizon-rust/cosmetics-fx/runtime
🤖 Agent: runtime layer, retargeted to main after #10140. follow.rs (frame-rate independent lag, snap after a teleport, kept out of the crosshair cone and clear of the head) and trail.rs (spawns by distance walked, 24 particles per player and 512 in all) are pure horizon-cosmetics code, plus the fx_bench example. Nothing in the game calls them yet. Verified on this head: clippy -D warnings and 68 horizon-cosmetics tests.
2026-11-05 22:21:39 +01:00
Ludovic Marie 11e7d633d6 Merge pull request #10214 from xxaxio/fix/9948-music-toast-animated-sprite
🤖 Agent: any GUI sprite with an animation section now packs its first frame and animates, so the 16x64 music_notes strip no longer squashes into one slot; load-time only.
2026-11-05 22:21:35 +01:00
Ludovic Marie 8dcf45e95e Merge pull request #10171 from xxaxio/fix/9957-sulfur-cubes-parity
🤖 Agent: sulfur cube blast is TNT-interaction only with mobGriefing (NONE without), ENTITY_INTERACT sourced by the cube, tnt.disabled overlay and used stat.
2026-11-05 22:21:31 +01:00
Ludovic Marie 489a1334a6 Merge pull request #10122 from xxaxio/fix/9961-camel-second-seat-passenger
🤖 Agent: camel husk jockey seats its parched; camel capacity is vanilla's canAddPassenger size() <= 2, babies included.
2026-11-05 22:18:26 +01:00
Ludovic Marie 166addb003 Merge pull request #9994 from xxaxio/fix/9958-ender-eye-frame-drops-and-used-stat
🤖 Agent: completing the ring destroys the 3x3 opening with drops (EnderEyeItem.useOn destroyBlock(pos, true)) and awards ender_eye used.
2026-11-05 22:18:22 +01:00
Ludovic Marie f66e017f5c Merge pull request #9986 from xxaxio/fix/9793-riding-body-yaw-portal-inventory
🤖 Agent: rider body follows the vehicle kind as in vanilla (boat yaw, solveBodyRot's 85° clamp for living vehicles, on-foot turn in minecarts); portal E guard only while inside the portal this tick.
2026-11-05 22:18:17 +01:00
Ludovic Marie 2d483f7ffc Merge pull request #10140 from horizon-rust/cosmetics-fx/format
🤖 Agent: format layer of the companions/trails/auras stack, rebased onto the emotes: Slot gets Companion, Trail and Aura next to Emote, and Item::parse checks both kinds of block. Item::animate also builds its animated-part table once at parse instead of searching by name every frame, which saves about 0.3 ms a frame at 64 wearers (profiled). Verified on this head: clippy -D warnings on game and launcher, 60 horizon-cosmetics tests; nothing draws the new slots yet.
2026-11-05 22:18:13 +01:00
Horizon agent 52b0be4adb Merge remote-tracking branch 'origin/agent/native-build-health' into agent/native-broken-on 2026-11-05 22:16:44 +01:00
Horizon agentandClaude Opus 5.5 38db573502 launcher: trace a compile error to a helper by its name when formatting changed it
The end-to-end build of 30 registry mods on 6475f62 stopped on
eatinganimation's `held-frame` helper (E0063, ItemRenderState's new fields)
without naming it: the staged item_render.rs carries the helper with a
trailing comma rustfmt added, so its tokens no longer equal the plan's
helper_source. A helper is now also the function of its name in the staged
file, when the pristine source has no function of that name; two mods'
helpers of one name still name neither.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:16:23 +01:00
Horizon agentandClaude Opus 5.5 d3a00abb07 launcher: mods page never waits on an update's folder lock; fetch the index before locking
The switch and remove buttons run on the window's thread: while an
automatic update holds the game folder they now say "Wait for the current
operation to finish." instead of freezing the launcher until its downloads
end. auto::run takes the lock after the registry index arrives.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:52 +01:00
Horizon agentandClaude Opus 5.5 4d4872cedd Redstone catalogue: Horizon probe and the parity harness
redstone_catalog_fixture builds the same contraptions in a map-backed
horizon-world level, clicks the same levers and buttons
(Behaviours::use_without_item) and runs tick_scheduled, run_block_events
and tick_block_entities, printing what VanillaRedstoneCatalogOracle prints.

tools/test_redstone_catalog_parity.py reads the catalogue, downloads the
archive's schematics into .local/lra-catalog/ (pinned to da4dd51, one
sha256 over all 188), turns each entry into a case (switches off except
those the catalogue held on, a neighbour update on every block, then input
steps: every vector up to 8 bits, random ones beyond, random changes for
stateful builds) and compares both sides step by step. verified entries
must match; partial and unverified ones are reported. Entries whose inputs
are analog levels, lecterns or bare wire are listed as unsupported for
now. Three shards; --full runs larger sweeps and every vector up to 16
bits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
Horizon agentandClaude Opus 5.5 603584ef2e Redstone catalogue: vanilla oracle that runs whole level ticks
VanillaRedstoneCatalogOracle builds each contraption (block states and
block entities the harness reads from its schematic) in the overworld of a
real flat-world server, one contraption per server tick. Comparators keep
their saved output, containers and lecterns get real items (set silently:
no comparator ticks, no lectern pulse), and the comparator level of every
block entity is printed for the Horizon probe. The game clock keeps
running between contraptions (LevelTicks orders chunks by trigger time);
the probe starts from the time printed.

Levers and buttons are driven with useWithoutItem, and each tick runs what
ServerLevel.tick runs for blocks: scheduled block and fluid ticks, block
events and block entities, so pistons move. After each tick it prints the
outputs that changed, and at the end of each step the tick it ended on,
whether the level went quiet and a hash of the contraption's box. A tick
budget per contraption (the probe counts the same) bounds machines that
never settle; containers at or next to hoppers, droppers, dispensers and
crafters are watched every tick, and a changed level marks the run as not
comparable (the probe does not move items). HORIZON_CATALOG_TRACE=1 adds
every block tick and every block at a step's end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
Horizon agentandClaude Opus 5.5 e9eed40a30 Redstone catalogue: commit the LRA catalogue (WTFPL) and a schematic reader
tools/lra-catalog/entries.jsonl is maybeachromium's catalogue of the Logical
Redstone Archive (#10103): input and output positions, encodings and checks
for every entry, one entry per line. They shared it under the WTFPL
(LICENSE). The archive's schematics have no licence and stay out of the
repository.

tools/lra_schem.py reads Sponge schematics (versions 1 to 3), written from
the format's specification.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 22:15:27 +01:00
Horizon agent 20dc224154 Merge remote-tracking branch 'origin/main' into cg53-pr-10220 2026-11-05 21:58:37 +01:00
Horizon agentandClaude Opus 5.5 7a08446cfe sulfur cubes: the blast keeps blocks without mobGriefing; the cube sources ENTITY_INTERACT
SulfurCube.tickFuse explodes with ExplosionInteraction.TNT only when
mobGriefing is on (NONE otherwise), and equipItem's gameEvent has the
cube, not the player, as its source. Drop tests that checked constants.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:58:29 +01:00
Horizon agentandClaude Opus 5.5 0a2d899426 mobs: camel passengers follow Camel.canAddPassenger (size <= 2, babies too)
Vanilla's Camel.canAddPassenger accepts a rider while two or fewer ride,
with no baby check. Drop two tests that only checked their own inputs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:58:03 +01:00
Horizon agent 9e317340d9 Merge remote-tracking branch 'origin/main' into cg53-pr-10171 2026-11-05 21:58:03 +01:00
Horizon agentandClaude Opus 5.5 2228fd7be1 end: propagate the level events' error; drop a test that checked its own loop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:57:23 +01:00
Horizon agent 188ad5b2d1 Merge remote-tracking branch 'origin/main' into cg53-pr-10122 2026-11-05 21:57:23 +01:00
Horizon agentandClaude Opus 5.5 9e239c1c0c perspective: rider body follows vanilla per vehicle kind; portal key guard only while inside
A boat sets its passenger's body to its yaw (AbstractBoat.clampRotation);
a living vehicle's rider is drawn from the vehicle's body with the head at
most 85 degrees off and a fifth of the offset past 50
(LivingEntityRenderer.solveBodyRot). Minecarts and cushions keep the
on-foot turn, as vanilla's LivingEntity.tick does. The inventory key is
held back only while the last tick found the player in the portal, not
during the effect's fade-out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:57:16 +01:00
Horizon agent 78c6d024dd Merge remote-tracking branch 'origin/main' into cg53-pr-9994 2026-11-05 21:57:16 +01:00
martipopsandClaude Opus 5.5 2cce72df97 Merge upstream/main into controller/buttons-prompts-v2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:56:51 -05:00
Horizon agent 54fd1a76d8 Merge remote-tracking branch 'origin/main' into cg53-pr-9986 2026-11-05 21:56:02 +01:00
Ludovic Marie 5849fa33b3 Merge pull request #10104 from horizon-rust/mods/in-game-browser
🤖 Agent: merging the in-game mod browser. The point to check is that install never trusts the download: each file goes through horizon_mods::index::check_file (size, SHA-256, signature, manifest against the index) before it reaches horizon-mods/, and revoked files are refused. Tests pin both cases with a real .hmod.
2026-11-05 21:46:40 +01:00
Horizon agentandClaude Opus 5.5 f23fd18f41 Cosmetics: companions, trails and auras in the Wardrobe
The Wardrobe's picture (horizon_cosmetics::preview, its live view
included) draws a companion at its anchor, an aura see-through at the
feet and a trail as a short run of particles behind; the Wardrobe gets a
row for each slot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:38 +01:00
Horizon agentandClaude Opus 5.5 725efc0eca Cosmetics: draw companions, trails and auras in the game
A companion is posed on no bone: it follows its anchor with its lag,
keeps clear of the head and, for the player's own in first person, out of
the middle of the view. Trails spawn by distance walked and are drawn as
camera-facing particles; auras blend at the feet with their back faces
dropped. Trails and auras of every player go in one see-through span after
the players. Others' follow "Other players' cosmetics" and "Hide other
players' trails and auras". Nothing is an entity or reaches a server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:37 +01:00
Horizon agentandClaude Opus 5.5 f17a44249f Cosmetics: "Hide other players' trails and auras"
The setting that was "Show other players' auras and trails" (off, for
items that didn't exist) becomes a hide switch, off by default: others'
trails and auras show like their other cosmetics unless the player hides
them. In the game's Horizon settings and the launcher's Wardrobe, the same
cosmeticsHideFx line of horizon-extras.txt; the old cosmeticsAuras line is
dropped on save.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:37 +01:00
Horizon agentandClaude Opus 5.5 7df444ab89 Cosmetics: how companions follow and trails spawn
follow.rs: a frame-rate independent lag toward the anchor, a snap after a
teleport, and the two rules that keep a companion out of the way (out of
a cone round a first-person crosshair, clear of the head). trail.rs:
particles spawned by distance walked only, placed from their age alone,
24 per player and 512 in all. fx_bench measures their CPU cost for 1, 16
and 64 wearers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:35 +01:00
Horizon agentandClaude Opus 5.5 2a76fd1a00 Cosmetics: companion, trail and aura item format
Three new slots that ride no bone: a companion floats beside the player,
a trail leaves particles where they walk, an aura glows faintly at their
feet or round the body. Their blocks, limits and checks live in fx.rs;
item.rs reads them, refuses a bone on them and measures their extent.
Nothing draws them yet.

Item::animate no longer looks its parts up by name every frame (a tree
search and string compares per animation, for every wearer): the animated
parts are found once, when the item is read. At 64 wearers in view that
was 0.3 ms a frame on an i7-7500U.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:42:35 +01:00
martipopsandClaude Opus 5.5 9dae50aefd Controllers: bindable actions, prompts over gameplay, and button art
Held mappings are named by an Action rather than read by key, per device, and more actions can be bound: hotbar
slots, fullscreen, toolbars, social and quick actions. Held drop repeats and throws the stack. Readers of the
movement keys see the left stick, and a bumper steps the hotbar through the wheel's gates. Prompts over gameplay
are pad-driven and keep what's behind them, the keyboard opens on every field, and focus follows its widget.
Buttons and the crosshair draw from Horizon's own art, one file per pad family, instead of text labels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:37:30 -05:00
Horizon agentandClaude Opus 5.5 27c2e6457f Mods hot load: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:26:56 +01:00
Horizon agentandClaude Opus 5.5 eb55d13be3 horizon-report: overhead counts the tick's own spans; other worker jobs counted, not timed
On a smoke server (2 ms ticks) the overhead line charged the chunk pump's spans of the
wait between ticks (1,788 a tick) to MSPT. Only the spans the tick records count now,
and a dedicated server records spans anyway for its slow-tick line (HORIZON_SPIKES,
on by default), so they are listed apart and left out of the report's own cost.
The 'other' worker jobs ran at 278k/s for 0.0 ms each: two thread-CPU clock reads
each cost more than the job; they are counted only. A sum of no players' rates no
longer reads -0.0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:26:41 +01:00
Horizon agentandClaude Opus 5.5 50d9f3d7f7 launcher: warn before installing a native mod that doesn't build on your Horizon
The registry's daily check (horizon-mods native-compat.yml) marks the
versions horizon-mixins refuses on the newest Nightly with `broken_on`
({horizon, code}) in index.json. The browser's fit line now says
"Doesn't build on the Horizon you play (CODE): a build leaves it out until
its author updates it" when that Horizon is the one the mod would build on.

- horizon-mods index: `Version.broken_on` (empty and absent by default).
- native_fit: `Fit::Broken { code }` for a hook mod, or a mod pinned to the
  played commit, when an entry names that commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:19:51 +01:00
foudretbr f15ca8d82a Preparing ignores the player's global Java options
A collector chosen in JAVA_TOOL_OPTIONS or _JAVA_OPTIONS stopped every JVM
horizon-prepare starts, as they pass -XX:+UseSerialGC: "Multiple garbage
collectors selected", so the version was never prepared. java::command, which
they all go through, now removes those variables and JDK_JAVA_OPTIONS.

Fixes #10067
2026-11-05 21:17:58 +01:00
AxxioandAntigravity 46d83389ac launcher: serialize mod mutations per game home (fixes #10072)
Background automatic registry updates and manual mod operations (install,
remove, enable, disable) could mutate the same game home folder concurrently.
Because auto-update loads `mod-browser.json` before downloading and saves upon
completion, a manual install or removal occurring during the download would
be overwritten by the stale snapshot upon completion, losing manually added
mods or reviving removed mods. In addition, temporary save files could collide.

Introduce a per-game-home re-entrant mutex (`browser::lock::lock_home`)
covering mod mutation critical sections across background auto-updates and
manual operations. Use unique atomic temporary file paths during save to
prevent collision.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 21:11:12 +01:00
Horizon agentandClaude Opus 5.5 422c36b236 wire stream: eat's bites last 10 ticks, far from the server's 32
A loaded server catching up ran a 20-tick bite's remaining ticks in a burst and finished the apple.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:08:47 +01:00
Horizon agentandClaude Opus 5.5 532421c5f0 launcher: leave out the native mods that break a build, not the build
One mod that horizon-mixins refuses or that doesn't compile used to fail the
whole native build (#horizon-support 2026-11-05: eatinganimation's moved
target, hitbox_plus's support module, worldedit's moved chat.rs). The build
now leaves that mod out, with the mods that need it, and builds again:

- isolate.rs names the mod a horizon-mixins diagnostic is about (mod_id, its
  manifest, its support module's name or destination), or the mods the
  compiler's errors were traced to, and adds their dependents.
- A refusal costs one horizon-mixins run; a compile failure one compile, at
  most 3 times. A server engine export still fails as before.
- native-mods.json records `left_out`, so the same mods turned on read as up
  to date, and the Mods page says "X was left out: made for another Horizon
  version / clashes with Y / it needs Z, which was left out" (86 languages).
- MIXINS_REV → 5a64af9: horizon-mixins#27 resolves `horizon_client::…`
  targets that moved into horizon-client-lib, #28 follows workspace globs for
  a module's name and puts the mod id on module errors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:58:01 +01:00
AxxioandAntigravity cfccbb8f40 hud: slice animated GUI sprites to first frame in atlas packing (fixes #9948)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 20:54:48 +01:00
Horizon agent e78ed4a7a1 Merge remote-tracking branch 'origin/main' into pvp/hud-editor-main 2026-11-05 20:43:04 +01:00
martipopsandClaude Opus 5.5 db65974cd8 Controllers: new default buttons
New players get the new layout. A file that bound a button by hand keeps it through the change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:40:02 -05:00
DUzzL a13106bc0b Format container tooltip lines with numbered translation arguments 2026-11-05 20:35:02 +01:00
DUzzL 878342f98e Keep a synched horse rear until its flag clears 2026-11-05 20:22:16 +01:00
Ludovic MarieandClaude Opus 5.5 e6d6ce8df2 Revert the command registries change: not shown to work in game
Commands typed in the window parse tags against the window's own tags,
which no server packet updates; that gap predates live switching and
needs its own change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:14:15 +01:00
foudretbr 1f15474046 Launcher: the proxy hint names SOCKS in every language
The Settings hint now reads (http://host:port, socks5://host:port). Each language keeps its translation, without the word HTTP and with the socks5 form added; i18n's tests want every string in every table.
2026-11-05 20:14:11 +01:00
foudretbr 114dbd0e21 Launcher: downloads can go through a SOCKS proxy
Turn on ureq's socks-proxy feature and stop refusing SOCKS addresses, in port-pack's parse and the launcher's settings. socks5h, curl's name for SOCKS5 with remote DNS, is read as socks5, which already works that way in ureq. A test runs a request through a stand-in SOCKS5 proxy.

Fixes #8143
2026-11-05 20:14:10 +01:00
DetimesandClaude Opus 5.5 6e083af988 Native mods: crates/horizon-mod-deps puts mysql, redis, tungstenite and rusty-opus in Cargo.lock (#10174)
A workspace member with no code whose dependencies are all optional: Cargo
locks them, so a native mod's crate may name them, and no build compiles them
(horizon-client's dependency tree, default and --no-default-features, is
unchanged). All four are pure Rust and use the rustls + ring already locked.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:11:58 +00:00
martipopsandClaude Opus 5.5 d8781cb9df Splitscreen: a player's pack reload no longer deletes P1's overlay
Fixes #10179.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 14:10:42 -05:00
Horizon agentandClaude Opus 5.5 abf29eb414 Multiplayer: a click lists its changed slots in the order vanilla's hash map iterates
MultiPlayerGameMode.handleContainerInput puts the changed slots into a new fastutil
Int2ObjectOpenHashMap and ServerboundContainerClickPacket writes them as it iterates: the key 0
first, then the table from its last position down. Horizon listed them in slot order. The wire
stream harness's inventory scenario caught it: a shift-click from slot 37 into slot 10 went out
37, 10 from vanilla and 10, 37 from Horizon, a byte-level difference any server can see.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:08:04 +01:00
Horizon agentandClaude Opus 5.5 4a5450c6f8 horizon-report: is_multiple_of (clippy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:02:47 +01:00
Horizon agentandClaude Opus 5.5 d216a5477d horizon-report: loose entities of every level; rustfmt
The items, orbs, projectiles, minecarts and boats were counted for the level in place
only (the End, on the SMP, when the report ends), so tick_boats' 3 ms had no count
beside it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:00:16 +01:00
Horizon agentandClaude Opus 5.5 216a6218af wire_stream: compare Horizon's Legacy combat feel with Modern, packet for packet
--clients horizon,horizon-legacy plays every scenario with two Horizon clients, the
second with the Legacy feel and every combat extra on, the 1.8 rules included, and
compares their serverbound streams as the vanilla comparison does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:59:02 +01:00
Horizon agentandClaude Opus 5.5 5f69e3a789 Mods: commands see the tags a live switch or a data pack added
The command registries took item and block tags from the resources the
game started with and were built once per world, so /execute and
/give refused a tag a switched mod added (seen in game: Unknown item tag
'c:gems/glint'). They now read the world's tags and are built again
after a live switch's data reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:57:44 +01:00
AxxioandAntigravity be42c5d25e game, mobs: align sulfur cube prime, vibrations and TNT drop decay with vanilla (fixes #9957)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 19:56:42 +01:00
Horizon agentandClaude Opus 5.5 ef4463a476 horizon-report: spans inside tick_lan and tick_sculk_entities
On the official SMP, 12.6 of tick_lan's 14.7 ms a tick and all of tick_sculk_entities'
6.4 ms had no span below them. Name the steps: guest ticks, entity states, the hub's
level pass (world sync, chunks, block changes, containers, entity tracking, player
state), the end of the tick and the host events; sculk's prepare, wardens, step-on and
entity game events. Spans cost nothing while off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:56:42 +01:00
Horizon agentandClaude Opus 5.5 ee7a6dd9c8 wire stream: dig_teleport compares the ticks after the teleport aligned on its acceptance
The client sends /tp at a fixed tick, but a loaded server answers 2-6 ticks later from one run to the
next (seen with two identical Horizon clients). The ticks before the teleport compare one to one; from
the client's AcceptTeleportation on they align on it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:54:30 +01:00
Horizon agentandClaude Opus 5.5 ab9929b5ab horizon-report: per-mob-type cost, level spans, chunk workers, network, overhead, stop/status
The official SMP is mob-bound (5,400 mobs, 73 ms a tick at 60 players) but the report
only said tick_creature_entities took the time, not which mobs. Format 2 adds, without
a restart and at a measured cost:
- mob_costs: one tick in four times every mob's tick and sums it by dimension and type
  (ms a tick, mobs ticked, us a mob);
- level_* spans: each dimension's share of the server tick;
- chunk_workers: streaming::job_stats while recording (load/generate/decorate/light...);
- network: wire bytes in/out per player and in all (connection counters, no new
  per-packet cost);
- overhead: spans and mob samples a tick at their price measured on the machine;
- horizon-report stop / status.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:47:54 +01:00
KBroneZandClaude Opus 5.5 7fcf18b6df test: /forceload add loads the chunk for the next command
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:38 +02:00
KBroneZandClaude Opus 5.5 ed01ae3029 fix: handle the container loot table event in the block update probe
Goes with "keep a placed template's container loot table".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:37 +02:00
KBroneZandClaude Opus 5.5 a9c7b82772 fix: load chunks in /forceload add, as vanilla does
`ServerLevel.setChunkForced` calls `getChunk` when a chunk becomes forced, so a
command right after `/forceload add` finds it loaded. Horizon only queued it, so
`execute in minecraft:the_nether run function` with `forceload add`, `place
jigsaw` failed with "That position is not loaded" and a skyblock pack's nether
island never appeared.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:37 +02:00
KBroneZandClaude Opus 5.5 2f9ec5be67 test: a placed template's chest keeps its loot table
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:36 +02:00
KBroneZandClaude Opus 5.5 cf31690cae fix: keep a placed template's container loot table
`LevelAccess::load_block_entity` decoded a template's block entity, and that
drops `LootTable`, so a chest placed by `/place template`, `/place jigsaw` or a
structure block came out empty. A skyblock pack's starter chest has its items in
a loot table. Report the table as a level event and hand it to `take_loot_table`,
as `/setblock` and `/data` do (`RandomizableContainer.tryLoadLootTable`).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:36 +02:00
KBroneZandClaude Opus 5.5 5062da5567 fix: upgrade a 26.2 noise with no octaves to a silent 26.3 noise
Void packs turn off vanilla's iceberg noises with
`{"firstOctave": -1000, "amplitudes": []}`. The upgrader left this as it was and
called it "more than 32 octaves or negative amplitudes"; 26.3 cannot read it, so
the world failed to start with `invalid world generation field: base_octave`.
With no octaves the noise is 0 everywhere, which 26.3 writes as one muted octave:
`{"base_octave": 0, "amplitude_modifiers": [0.0]}`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:34 +02:00
KBroneZandClaude Opus 5.5 69ecedfc01 fix: read default_block and default_fluid in 26.3's block state form
`state()` in the generator read only `{"Name", "Properties"}`, or the id alone,
so noise settings with `{"id", "properties"}` failed the world start with
`invalid generator resources: block state`. That is the form 26.3 uses and the
one `horizon-port-pack upgrade-datapack` writes for any non-default state. Decode
these two fields with `BlockStateJsonCodec`, as the rest of worldgen does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 20:44:34 +02:00
DUzzL f2dfddea11 Handle F3 debug keys on the title screen 2026-11-05 19:44:34 +01:00
Horizon agentandClaude Opus 5.5 b5ed12bc33 wire stream: compare a click's state id as its lag behind the server; hotbar_swap swings at the sky
A run's state ids count every change its server made since the login, so a click numbered 53 in one
run and 55 in the other were both up to date. hotbar_swap now also taps a number key with a swing at
nothing, where vanilla sends the slot only on the next tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:43:04 +01:00
Horizon agentandClaude Opus 5.5 e5105f07eb Pick: aim with vanilla's table trigonometry, so a placement's cursor matches to the bit
GameRenderer.pick aims along Entity.calculateViewVector: float radians through Mth.sin and Mth.cos,
the 65536-entry table. Horizon aimed with exact double trigonometry, which moved the hit point a
placement sends (ServerboundUseItemOnPacket's cursor) and an interaction's point by up to ~1e-4 of a
block. The wire stream harness's bridge scenario showed it: same position and look, cursor z
0.7173712 from vanilla, 0.7173119 from Horizon. Anticheats that recompute the cursor from the ray
compare it to vanilla's arithmetic. The other view_vector callers (projectiles, dragon, boats...)
are left for their own checks.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:42:49 +01:00
martipopsandClaude Opus 5.5 f513a784f0 Merge upstream/main into fix/pad-e2e-linux: #9946 edited the same file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:34:34 -05:00
Horizon agentandClaude Opus 5.5 a14ca6acf4 wire stream harness: headless slot clicks, the reset's self-teleport, signed block positions, two short bites
- click-slot without a renderer clicks the menu itself (the vanilla driver's handleContainerInput); horizon-ctl's
  clicks where the slot is drawn, which --headless never does.
- The reset's last teleport is the player onto itself: from the console `~` is the world spawn, which put the
  player inside bridge's stone.
- BlockPos.asLong decodes with each part signed.
- eat cuts its two bites short: when a full one ends is the server's entity event, late on a busy server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:00:39 +01:00
Horizon agentandClaude Opus 5.5 cc7200cd7a Multiplayer: the held slot goes out with the packets vanilla's game mode guards, not with any click
MultiPlayerGameMode.ensureHasSentCarriedItem runs in attack, interact, useItem, useItemOn,
releaseUsingItem, dropItem, piercingAttack and continueDestroyBlock only. A swing at nothing,
the start of a dig or an offhand swap sends no slot: a number key pressed in that tick goes out
at the start of the next one (MultiPlayerGameMode.tick). Horizon sent the slot whenever the tick
had a click. The wire stream harness's hotbar_swap scenario caught it: a tap of 2 with a swing at
the air sent SetCarriedItem before the Punch, a tick before vanilla.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:59:36 +01:00
AxxioandAntigravity 8c3f6e7d7b Seat camel husk jockey's parched in camel second seat (fixes #9961)
Co-Authored-By: Antigravity <[email protected]>
2026-11-05 18:57:44 +01:00
DUzzL 4272d9ae41 Mobs on a ladder or vine are not pushable; no crit while climbing 2026-11-05 18:47:31 +01:00
Horizon agentandClaude Opus 5.5 e26dae0527 wire stream harness: 15 minutes for a loaded machine's server to start
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:45:08 +01:00
Horizon agentandClaude Opus 5.5 186a478e21 test_wire_stream: load the decoder by path, so tools/ never shadows a tests/ module
tools/ first on sys.path made unittest discovery import tools/test_world_spawn_parity.py for
tests/test_world_spawn_parity.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:44:02 +01:00
Horizon agentandClaude Opus 5.5 0068b79fc4 parity: wire_stream runs when named (and nightly), alone
It drives a vanilla client on a virtual screen for ~15 minutes: no automatic selection on every client change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:26:04 +01:00
Horizon agentandClaude Opus 5.5 0744579340 wire stream harness: give a loaded machine's vanilla client 15 minutes to reach the world
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:25:03 +01:00
Horizon agentandClaude Opus 5.5 f2f3363a0a wire stream harness: start a scenario once the client has taken the reset's teleport
A busy server ran hit_target's reset teleport on the scenario's first tick: the vanilla client's first move
said off the ground and every later move differed. The reset now ends with a teleport, and the scenario waits
until the client has accepted it and ticked 40 more times.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:09:23 +01:00
Horizon agentandClaude Opus 5.5 a6730f376e Native mods switch: rustfmt and a doc lint
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:07:43 +01:00
Horizon agentandClaude Opus 5.5 aa6fc60c4a Merge mods/in-game-browser into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:51 +01:00
Horizon agentandClaude Opus 5.5 315d1e424d Mods: a live switch's data reaches the server with no data pack
The server built its world tables from the content root only when the
world had data packs; without one it kept the tables loaded at startup,
so a switched mod's recipes and tags never applied (seen in game:
/recipe give still said Unknown recipe). After a live switch the tables
are built from the new root. An install that waits for a restart says so
on the project page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:06:51 +01:00
Horizon agentandClaude Opus 5.5 b1e41d46ec Mods browser: its search box says it searches mods
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:05:56 +01:00
martipopsandClaude Opus 5.5 7a882e8442 pad_menu_e2e: pass on a fast machine and at 16:9, and say when the window isn't
focus_on looked a widget up before stepping, so a row scrolled out of view
ended the case: it now steps until the row is drawn. optimize_world's job
finished before the case could look on a fast machine: 60000 linked regions
take long enough and cost almost nothing. A tiling compositor sizes the hidden
window too, which fails every case that scrolls, so the harness warns.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:59:35 -05:00
Horizon agentandClaude Opus 5.5 b4644568dd wire stream harness: an absolute --bin, since horizon-ctl runs from the scratch home
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 44c13ed7ac vanilla_client: this machine's natives when prepared.json lists another's
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 6039812a76 VanillaTickScript: 26.3's SDL key and mouse codes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 93fc52b7ef wire stream harness: the vanilla client on the virtual screen, with Mesa's EGL
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 875f62b6b5 Wire: compare the client's packets with vanilla's, tick by tick
tools/test_wire_stream_parity.py: a Horizon client and a real vanilla 26.3
client join a vanilla server through tools/net_capture.py and play the same
tick-indexed scenarios (walk, sprint-jump, strafe, w-tap, sneak, hits,
sprint combos, block-hit, rod, bow, eat, bridging, inventory, hotbar swaps,
a teleport during a dig). tools/wire_stream.py cuts each serverbound stream
into client ticks (ClientTickEnd ends one) and decodes the packets; every
tick must hold the same packets with the same fields.

Horizon plays a scenario from HORIZON_TICK_SCRIPT (game/tick_script.rs),
before each client tick; vanilla from VanillaTickScript.java, which hooks a
fixed point of Minecraft.tick (Tutorial.onLookAt) and presses the keys
through KeyMapping.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:57:02 +01:00
Horizon agentandClaude Opus 5.5 eb7cc8305c Multiplayer: a rider's sprint and a boat's paddles go out in vanilla's order
LocalPlayer.sendChanges sends a passenger's look, then the vehicle it
controls, and only then its sprint (sendIsSprintingIfNeeded); riding a
vehicle it doesn't control, it sends no sprint at all. AbstractBoat.tick
sends the paddles from the boat's own tick, before its passenger's
sendChanges. Horizon sent the sprint first and for any vehicle, and the
paddles last.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:53 +01:00
Horizon agentandClaude Opus 5.5 9e7b50b560 Multiplayer: a new held slot goes out at the start of the next tick, as vanilla
MultiPlayerGameMode.tick, first in Minecraft.tick and run with a screen
open too, sends the slot chosen since the last tick
(ensureHasSentCarriedItem); LocalPlayer.sendChanges sends no slot. Horizon
also sent it at the end of the tick that changed it, before the input and
the move: a number key pressed in a tick went out one tick early, on the
other side of the movement packet a translating proxy (ViaVersion to an
older server) turns into that tick's flying packet. An action in the same
tick still sends the slot first, as ensureHasSentCarriedItem does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:53 +01:00
Horizon agentandClaude Opus 5.5 635fa28928 Multiplayer: a teleport aborts the dig on the wire, and a new level counts predictions from 0, as vanilla
ClientPacketListener.handleMovePlayer accepts the teleport, then runs
MultiPlayerGameMode.stopDestroyBlock: a dig the teleport interrupts (an
anticheat's setback while mining or bridging, a /tp) sends
ABORT_DESTROY_BLOCK and starts the attack strength again. Horizon dropped
the dig without a word, so the server kept counting it.

BlockStatePredictionHandler lives in the ClientLevel: handleLogin (a proxy's
server switch) and handleRespawn into another dimension make a new one, so
the use and dig sequence numbers count from 0 again. Horizon counted on for
the whole connection.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:56:52 +01:00
martipopsandClaude Opus 5.5 7ac3021dfb Show each controller's mapping source and uuid in pad status
A mapping bug report needs to know whether gilrs found an SDL row for a pad
and which id such a row would need. pad status now lists mapping_source
(sdl, driver or none) and uuid (32 hex digits, the id an SDL row starts with).

With splitscreen on, PadRouter holds the gilrs reader, so pad status said
backend:false and listed no controllers, unless the reader was lent to P1.
The router now leaves what it sees for the status of each game that has a
control socket; that list is only built when one is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:53:03 -05:00
Horizon agentandClaude Opus 5.5 56c896f51c docs/mods.md: which switches wait in a world
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:51:12 +01:00
Horizon agentandClaude Opus 5.5 aa95310c72 Mods: in a world, content mods of assets and data stay switchable
The Mods page froze every content mod's toggle while a world was open.
Those with only assets and data now reload live, so their toggles stay
active in a world this game serves; the others keep the lock. Each mod
is classified once when the mods are listed, and a native mod's toggle
no longer triggers a resource reload.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:50:21 +01:00
martipopsandClaude Opus 5.5 1a7a3678bb Refuse a control socket path too long for a Unix socket up front
A --control-socket path over the system's limit was only found when the
game tried to listen, after loading resources: it logged the raw SUN_LEN
error, wrote a crash report and exited 1, leaving startedCleanly:false so
the next start reset fullscreen. It is a bad command line: exit 2 with a
plain message, before the game folder, options.txt or resources are
touched. horizon-ctl launch refuses it as usage too, without starting a
game or creating a log beside the socket. A .json endpoint is TCP and has
no such limit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 11:45:34 -05:00
Horizon agentandClaude Opus 5.5 bdfe6e4dd1 Mods hot load: rustfmt and clippy fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:39:33 +01:00
Horizon agentandClaude Opus 5.5 b90cebc01b Merge main into mods/native-switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:29:20 +01:00
Horizon agentandClaude Opus 5.5 d9e35999b6 Merge main into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:29:04 +01:00
Horizon agentandClaude Opus 5.5 249aa0d4c1 Merge mods/in-game-browser into mods/hotload-content
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:25:38 +01:00
Horizon agentandClaude Opus 5.5 8aba8be7a9 Mods browser: rustfmt and a clippy fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:24:58 +01:00
Horizon agentandClaude Opus 5.5 3fc5f274be PvP & HUD: one stand-in line per widget in the editor
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:22:42 +01:00
Horizon agentandClaude Opus 5.5 3f164ac06a PvP & HUD: the HUD editor in main's new screen and menu matches
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:18:51 +01:00
Horizon agentandClaude Opus 5.5 ece613e260 PvP & HUD: document the HUD editor; it frames only drawn widgets
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:41 +01:00
Horizon agentandClaude Opus 5.5 5b52010b12 PvP & HUD: a HUD editor to drag, snap and resize the widgets
Edit HUD on the PvP & HUD page (in a world) or the HUD editor key
(unbound at first) opens a screen over the game: each widget in a frame,
dragged anywhere with snapping to the screen's edges and middle and to
the other widgets, the wheel resizing it, a right click sending it back
to its corner; buttons for text colour, shadow, boxes and opacity, Reset
and Done. Widgets with nothing to show yet stand in by their name. The
layout is saved to horizon-extras.txt as it changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:40 +01:00
AxxioandAntigravity 150423300c end: drop destroyed blocks and award used stat on portal completion (fixes #9958)
When the end portal frame ring is completed by placing the last eye of ender, vanilla destroys blocks in the 3x3 portal opening so that their drops and break effects occur instead of silently deleting them. Also awards minecraft:used for minecraft:ender_eye upon insertion into a frame.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 16:47:53 +01:00
Horizon agentandClaude Opus 5.5 c8a86851b9 Native mods: a trait mod turned off stops at once, and resumes when turned on
Options > Mods' switch now reaches the trait mods' dispatch: the game skips
a turned-off mod's Hooks from the next call, on the server thread and in
the window, and turning it on again resumes it with no rebuild while it is
in the build. Registration at start is untouched, so its content stays
registered; RenderHooks keep drawing until a restart; mixin code still
checks native_on.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:38:22 +01:00
Horizon agentandClaude Opus 5.5 408044e9a9 Mods: content mods of assets and data switch on and off without a restart
A content root is built per set of mods (.horizon-cache/mod-content/<stamp>),
so a running game keeps reading the root it was given while a switch
builds another; roots unused for a day are removed. Turning a mod on or
off in Options > Mods, or installing one from Browse Mods, applies at once
when the mod has only assets and data: the client reloads its resources,
then the server reloads its data packs as /reload does and resyncs its
players. A mod that adds blocks, items, mob effects, entity or menu types
or changes world generation keeps loading at start, and the Mods page
says to restart.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:36:58 +01:00
Horizon agentandClaude Opus 5.5 c0323c7707 Mods: browse Horizon's registry and install content mods from inside the game
Options > Mods > Browse Mods lists the registry's content mods for this
game (the index the launcher reads), searchable by name, tag and category,
with a page per project: authors, licence, categories, description and
changelog, Install or Update, its switch once installed, and its page on
the website. An install is checked as the launcher's (size, SHA-256,
signature, manifest against the index) and recorded in mod-browser.json,
so the launcher lists, updates and removes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:32:54 +01:00
Horizon agentandClaude Opus 5.5 e95658709a Design note: mods from inside the game, loaded and unloaded in seconds
What content mods can switch live (assets and data), what needs a world
reload (registries, worldgen), the in-game mod browser, and the native
fast path: every installed native mod built in and switched at run time,
with the dylib and wasm paths left to arismoko's fork.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:31:45 +01:00
AxxioandAntigravity 3c154d3bb6 perspective, game: align riding body yaw and block inventory in nether portals (fixes #9793)
In vanilla (\LivingEntity.tick\), a passenger's body yaw matches its vehicle's rotation
(or living body yaw), clamped to keep head yaw within [-85°, 85°], and does not update
limb swing / walk distance from vehicle motion.

Furthermore, \Minecraft.handleKeybinds\ blocks opening the inventory (\keyInventory\)
while in a portal transition (\handlePortalTransitionEffect\), preventing repeatedly
reopening the inventory screen while standing in nether portals.

- Compute ridden vehicle yaw for boats, minecarts, mobs, and cushions in \perspective.rs\.
- Align rider body yaw to vehicle rotation, clamped to [-85°, 85°] of head yaw.
- Disable walk distance accumulation when riding so the player model does not swing limbs.
- Guard \KeyCode::KeyE\ in \game.rs\ and \
emote.rs\ when in portal transition.

Co-Authored-By: Antigravity <[email protected]>
2026-11-05 16:31:29 +01:00
Mayonesu0andClaude Opus 5.5 dec18717de Merge upstream main: keep pregen and the renamed wheel key
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 16:24:31 +01:00
Mayonesu0andClaude Opus 5.5 429defdece Options: the wheel scrolls a settings list past its buttons
A vanilla list keeps the wheel: AbstractContainerWidget inherits
AbstractScrollArea.mouseScrolled, which scrolls and never reaches the rows, so
CycleButton.mouseScrolled only runs for a button outside a list. Horizon stepped
the row under the cursor, so scrolling a long page changed options.

"Scroll wheel changes settings" keeps that behaviour as an opt-in extra, off by
default. Its saved key is new because the old one was written as true by every
install, and its tooltip has a new key because other languages said "as in
vanilla"; they fall back to English until translated.

Fixes #9633

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 15:33:32 +01:00
AkirabaneandClaude Opus 5.5 db3dcf2dcc dedicated: log each player-* request, and document a hand-over in order
Each player-* request writes one log line with what became of it: INFO
and what it did, or WARN and the reason it was refused. The line names
the player and, for the export, the import, the confirm, the release and
a named drop, the hand-over id, so the lines of one hand-over can be
found on both servers. The record and the request's body are never
written.

docs/server.md gives a hand-over in order, with the ids, the commit
point and what undoes itself, and the log lines.

tools/server_transfer_e2e.py runs a whole hand-over between two
dedicated servers over their real control sockets: two engines with
[transfer] on and Velocity forwarding, a small relay per engine that
answers the forwarding request as a proxy does, and horizon-bots
clients. It follows the documented order step by step, checks the
target's player file is on disk when the confirm answers and that the
source writes nothing for a player moved away, then aborts a hand-over
back and checks the player plays on.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:57 +01:00
AkirabaneandClaude Opus 5.5 bc2363c93b dedicated: player-import takes the place the request names, and the view follows
The request may name where the player arrives, over the record's
compound: `position`, `rotation`, `on_ground` and, now that #8693 keeps
a guest's motion, `velocity`. They belong to the `position` part. The
pitch is taken as Entity.setXRot takes it (modulo a turn, then cut to
+-90) and the yaw as it is; a velocity component over 10 blocks a tick
is taken as 0, as Entity.load takes a saved Motion. A position must be
within the same bounds as the record's own (Entity.load's clamp:
30,000,512 on x and z, 20,000,000 on y), and a rotation or velocity must
be finite numbers.

When the import moves the player to another chunk than the one its view
was built around, the view moves at once: the chunk centre is sent
before the position to confirm, and the chunks out of reach are
forgotten. The reply says so with "recentered".

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:57 +01:00
AkirabaneandClaude Opus 5.5 f5aef42f71 lan: an arrival's join line uses the team and nickname display name
#9829 made the join and leave lines name a player as Player.getDisplayName
does (its team's format and its nickname). place_arrival, which announces
a player placed by player-import or player-confirm, still used the plain
name; it now uses player_display_name too, so an arrival is announced as
a login is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:46:56 +01:00
AkirabaneandClaude Opus 5.5 a66f0fc57d dedicated: a durable commit point, retries after it, and the record's bounds
martipops's second review of #8730.

The commit point now covers the three files and a power loss: the
player file, advancements and statistics are each written to a new
file, synced, then renamed into place, and the folder is synced on Unix
(save::write_durable, write_dat_durable). Each write returns its error,
and the player stays held and imported when one fails. Ordinary saves
are unchanged.

A retry of `player-import` or `player-confirm` after the commit, whose
reply was lost, names the hand-over of a player already placed from it:
it is answered ok, placed, with `"retried": true`, and nothing is done
again. Another hand-over's id is still refused.

The record's health is refused over 1024 and its absorption over 2048
(max_health and max_absorption), its food exhaustion over 40
(FoodData.addExhaustion) and an effect amplifier outside 0 to 255, as
26.3's bytecode bounds them. A stack's count needs no check:
item_from_nbt already takes one outside ItemStack's 1 to 99 as 1.

docs/server.md says what a proxy does when `moved` is refused after the
commit (the freeze ran out, or an operator's /kill), and that it
confirms only while the export's `freeze_ticks_left` leaves it time.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:27 +01:00
AkirabaneandClaude Opus 5.5 4a5c6ab5d1 dedicated: player-import and player-confirm let a held player in
`player-import <uuid> <json>` applies a hand-over's record (from
`player-export <uuid> freeze`) to the held player on the target, whole or
in parts, and tells its client what changed. Unless the request says
`"place": false`, it then places the player as `player-confirm <uuid>
<hand-over>` does.

Placing is the hand-over's commit point: the player file, advancements
and statistics are written to disk synchronously, then the player is
placed, and only then does the reply say ok. A proxy releases the
source after that reply, so a crash of the target no longer leaves the
only current copy in memory. If the file cannot be written the player
stays held and imported.

The record's hand-over id is bound to the held player at the import: a
record without one (an export without freeze) is refused, a retry of the
same record is taken and another hand-over's is not. `player-confirm`
names it, and `player-drop <uuid> [hand-over]` may: a named drop must
name the imported record's. A held player that is leaving (dropped or
kicked, its connection not closed yet) can be neither imported nor
confirmed.

The import reads the whole compound as a file load does: the keys
Horizon does not model, the mods' attachments (given to the game's own
map) and the body's values. The body follows the parts: fall and motion
with `position` (with the saved vehicle), air, fire, freezing and food
exhaustion and timer with `health`. Values no player can have are
refused before anything changes: a position, rotation, motion, health,
absorption, saturation, experience progress or fall that is not a
number, a position past vanilla's clamp, a health of zero or less.

The client is sent the effects the import removed and the recipe book
it brought. Every player gets the arrival's list entry, since every
player is shown its entity; `announce` is the join line only. At
confirm the client is sent its position and inventory again, since what
it did since the import was not acted on.

The control endpoint refuses a request line longer than 16 MiB, which
the game would otherwise read and parse between two ticks.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:27 +01:00
AkirabaneandClaude Opus 5.5 6fae1fdc44 Config golden: add max_hold_seconds to the [transfer] section
The config reply pinned by api-v1-config.json lists [transfer] with its
defaults; this PR adds max_hold_seconds (60 by default), so the golden
lists it too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:17 +01:00
rdbtCVSandCodex gpt-6.1-sol 764795ae20 docs(server): regenerate held-player transfer setting reference
Co-Authored-By: Codex gpt-6.1-sol <[email protected]>
2026-11-05 13:45:17 +01:00
AkirabaneandClaude Opus 5.5 62646f32ab lan: the login of an expected player is held, and player-drop lets it go
A login that used up a proxy's expectation (player-expect, taken as the
login starts) is held on the target server: it gets the whole join
sequence and the level around it, and its acknowledgements are handled,
but it is not placed. No join message, no list entry for the others, not
among the entities they are shown, not simulated, and nothing else it
sends is acted on. The world is loaded around a held player as around a
placed one. The log says "<name> is held: it arrives from another
server".

A held player nobody takes over is dropped after [transfer]'s
max_hold_seconds (60 by default, 1200 ticks), with one warning: a player
already kicked or dropped, whose connection is slow to close, is not
kicked again on every tick. horizon-server passes the setting with the
others.

A held player keeps a true picture of the others while it waits: a
player who joins meanwhile is added to its list, and one who leaves is
removed from it with its entity, or its client would drop the entity of
a player it has no entry for.

`player-drop <uuid>` forgets the player's expectation and disconnects
the held player without a save or a word to anyone. player-export
refuses a held player. Placing it is the import's work, next.

Refs #7484

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 13:45:16 +01:00
565 changed files with 29829 additions and 2441 deletions

No files matched your search

Generated
+244
View File
@@ -147,6 +147,12 @@ dependencies = [
"x11rb",
]
[[package]]
name = "arcstr"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d"
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -303,6 +309,21 @@ dependencies = [
"objc2 0.6.5",
]
[[package]]
name = "btoi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b5ab9db53bcda568284df0fd39f6eac24ad6f7ba7ff1168b9e76eba6576b976"
dependencies = [
"num-traits",
]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -329,6 +350,12 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
@@ -651,6 +678,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
@@ -768,6 +804,17 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
[[package]]
name = "derive_utils"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc05a5d33db20c784f873e84934ad94bb209a090987ac5f62fede2c178234f23"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "des"
version = "0.8.1"
@@ -1364,6 +1411,7 @@ dependencies = [
"serde_json",
"sha1",
"sha2",
"toml",
"unicode-bidi",
"ureq",
"wgpu",
@@ -1608,6 +1656,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "horizon-mod-deps"
version = "0.1.0"
dependencies = [
"mysql",
"redis",
"rusty-opus",
"tungstenite",
]
[[package]]
name = "horizon-mods"
version = "0.1.0"
@@ -2003,6 +2061,15 @@ dependencies = [
"generic-array",
]
[[package]]
name = "io-enum"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f859dc81e05e585e93cfc84dffa4fd068d0651ca229c51bca6101c056b63b93a"
dependencies = [
"derive_utils",
]
[[package]]
name = "ipconfig"
version = "0.3.4"
@@ -2250,6 +2317,12 @@ version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
[[package]]
name = "lz4_flex"
version = "0.14.0"
@@ -2339,6 +2412,59 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
[[package]]
name = "mysql"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090068bc0a24762663338da78414148333d1ee246a7293c10a517ddf1548b291"
dependencies = [
"bufstream",
"bytes",
"crossbeam-queue",
"crossbeam-utils",
"flate2",
"io-enum",
"libc",
"lru",
"mysql_common",
"named_pipe",
"pem",
"percent-encoding",
"rustls",
"rustls-pemfile",
"socket2 0.6.5",
"twox-hash",
"url",
"webpki",
"webpki-roots 1.0.10",
]
[[package]]
name = "mysql_common"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20e7f6a15265801679ddf1ddb38fd74b4555a677a78378d4561fbe5f0b1d4d72"
dependencies = [
"base64",
"bitflags 2.13.2",
"btoi",
"byteorder",
"bytes",
"crc32fast",
"flate2",
"getrandom 0.3.4",
"num-bigint",
"num-traits",
"regex",
"saturating",
"serde",
"serde_json",
"sha1",
"sha2",
"thiserror 2.0.21",
"uuid",
]
[[package]]
name = "naga"
version = "30.0.1"
@@ -2376,6 +2502,15 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "named_pipe"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b"
dependencies = [
"winapi",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2975,6 +3110,16 @@ dependencies = [
"hmac",
]
[[package]]
name = "pem"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
dependencies = [
"base64",
"serde_core",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -3302,6 +3447,22 @@ dependencies = [
"yasna",
]
[[package]]
name = "redis"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db7f3d75b5a3301746bff39a767bf4c5c42506b08513527308b3875bfb64a031"
dependencies = [
"arcstr",
"combine",
"itoa",
"percent-encoding",
"ryu",
"socket2 0.6.5",
"url",
"xxhash-rust",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -3443,6 +3604,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
@@ -3469,6 +3639,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "rusty-opus"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46604e6809c06441da1cfdde1f0cc25a4f2bf42c7d005aa387c2875c02d78762"
[[package]]
name = "ruzstd"
version = "0.9.1"
@@ -3478,6 +3654,12 @@ dependencies = [
"twox-hash",
]
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "salsa20"
version = "0.10.2"
@@ -3496,6 +3678,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "saturating"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -3715,6 +3903,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "socks"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
dependencies = [
"byteorder",
"libc",
"winapi",
]
[[package]]
name = "spake2"
version = "0.4.0"
@@ -4093,6 +4292,18 @@ version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
[[package]]
name = "tungstenite"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22"
dependencies = [
"bytes",
"log",
"rand",
"thiserror 2.0.21",
]
[[package]]
name = "twox-hash"
version = "2.1.5"
@@ -4168,6 +4379,7 @@ dependencies = [
"once_cell",
"rustls",
"rustls-pki-types",
"socks",
"url",
"webpki-roots 0.26.11",
]
@@ -4435,6 +4647,16 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
@@ -4630,6 +4852,22 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
@@ -4639,6 +4877,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.54.0"
+1
View File
@@ -39,6 +39,7 @@ members = [
"crates/horizon-optifine",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-deps",
"xtask",
]
resolver = "2"
+5
View File
@@ -0,0 +1,5 @@
# `keys.contains(&KeyCode::ShiftLeft)` reads a mapping by its default key: ask by `Action`, so the
# mapping is named and every device that can hold it counts.
disallowed-methods = [
{ path = "horizon_client_input::held::HeldKeys::contains", reason = "use `held(Action::..)`, or `raw` for a key that is no mapping's default", allow-invalid = true },
]
+146
View File
@@ -0,0 +1,146 @@
//! Every input the game names: vanilla's key mappings (`Options.keyMappings`) and Horizon's
//! pad-only actions. Held state is asked by [`Action`] ([`crate::held::HeldKeys::held`]), not by a
//! key: code that wants a mapping says which one, and every device that can hold it counts.
use winit::keyboard::KeyCode;
macro_rules! actions {
($($variant:ident, $name:literal, $key:expr;)*) => {
/// A key mapping, or a pad-only action.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Action {
$($variant,)*
}
impl Action {
/// In `Options.keyMappings` order, then Horizon's own mappings, the pad-only actions last.
pub const ALL: &[Action] = &[$(Action::$variant,)*];
/// The saved name: vanilla's for a mapping, `pad.*` for Horizon's own.
pub fn name(self) -> &'static str {
match self {
$(Action::$variant => $name,)*
}
}
/// The default key the game's held state keeps this action under. `None` for what
/// isn't held as a key there: a mouse mapping, one with no default, a pad-only action,
/// and the debug keys, which act on the physical key (`debug_keys.rs`) and whose
/// defaults are other mappings' keys.
pub fn key(self) -> Option<KeyCode> {
match self {
$(Action::$variant => $key,)*
}
}
}
};
}
actions! {
Attack, "key.attack", None;
Use, "key.use", None;
Forward, "key.forward", Some(KeyCode::KeyW);
Left, "key.left", Some(KeyCode::KeyA);
Back, "key.back", Some(KeyCode::KeyS);
Right, "key.right", Some(KeyCode::KeyD);
Jump, "key.jump", Some(KeyCode::Space);
Sneak, "key.sneak", Some(KeyCode::ShiftLeft);
Sprint, "key.sprint", Some(KeyCode::ControlLeft);
Drop, "key.drop", Some(KeyCode::KeyQ);
Inventory, "key.inventory", Some(KeyCode::KeyE);
Chat, "key.chat", Some(KeyCode::KeyT);
Playerlist, "key.playerlist", Some(KeyCode::Tab);
PickItem, "key.pickItem", None;
Command, "key.command", Some(KeyCode::Slash);
Friends, "key.friends", Some(KeyCode::KeyO);
SocialInteractions, "key.socialInteractions", Some(KeyCode::KeyP);
ToggleGui, "key.toggleGui", Some(KeyCode::F1);
ToggleSpectatorShaderEffects, "key.toggleSpectatorShaderEffects", Some(KeyCode::F4);
Screenshot, "key.screenshot", Some(KeyCode::F2);
TogglePerspective, "key.togglePerspective", Some(KeyCode::F5);
SmoothCamera, "key.smoothCamera", None;
Fullscreen, "key.fullscreen", Some(KeyCode::F11);
SpectatorOutlines, "key.spectatorOutlines", None;
SpectatorHotbar, "key.spectatorHotbar", None;
SwapOffhand, "key.swapOffhand", Some(KeyCode::KeyF);
SaveToolbarActivator, "key.saveToolbarActivator", Some(KeyCode::KeyC);
LoadToolbarActivator, "key.loadToolbarActivator", Some(KeyCode::KeyX);
Advancements, "key.advancements", Some(KeyCode::KeyL);
QuickActions, "key.quickActions", Some(KeyCode::KeyG);
DebugOverlay, "key.debug.overlay", None;
DebugModifier, "key.debug.modifier", None;
Hotbar1, "key.hotbar.1", Some(KeyCode::Digit1);
Hotbar2, "key.hotbar.2", Some(KeyCode::Digit2);
Hotbar3, "key.hotbar.3", Some(KeyCode::Digit3);
Hotbar4, "key.hotbar.4", Some(KeyCode::Digit4);
Hotbar5, "key.hotbar.5", Some(KeyCode::Digit5);
Hotbar6, "key.hotbar.6", Some(KeyCode::Digit6);
Hotbar7, "key.hotbar.7", Some(KeyCode::Digit7);
Hotbar8, "key.hotbar.8", Some(KeyCode::Digit8);
Hotbar9, "key.hotbar.9", Some(KeyCode::Digit9);
DebugReloadChunk, "key.debug.reloadChunk", None;
DebugShowHitboxes, "key.debug.showHitboxes", None;
DebugClearChat, "key.debug.clearChat", None;
DebugCrash, "key.debug.crash", None;
DebugShowChunkBorders, "key.debug.showChunkBorders", None;
DebugShowAdvancedTooltips, "key.debug.showAdvancedTooltips", None;
DebugCopyRecreateCommand, "key.debug.copyRecreateCommand", None;
DebugSpectate, "key.debug.spectate", None;
DebugSwitchGameMode, "key.debug.switchGameMode", None;
DebugDebugOptions, "key.debug.debugOptions", None;
DebugFocusPause, "key.debug.focusPause", None;
DebugDumpDynamicTextures, "key.debug.dumpDynamicTextures", None;
DebugReloadResourcePacks, "key.debug.reloadResourcePacks", None;
DebugProfiling, "key.debug.profiling", None;
DebugCopyLocation, "key.debug.copyLocation", None;
DebugDumpVersion, "key.debug.dumpVersion", None;
DebugProfilingChart, "key.debug.profilingChart", None;
DebugFpsCharts, "key.debug.fpsCharts", None;
DebugNetworkCharts, "key.debug.networkCharts", None;
DebugLightmapTexture, "key.debug.lightmapTexture", None;
DebugImprovedTransparency, "key.debug.improvedTransparency", None;
Emote, "key.emote", Some(KeyCode::KeyB);
HotbarPrevious, "pad.hotbarPrevious", None;
HotbarNext, "pad.hotbarNext", None;
}
impl Action {
pub fn parse(name: &str) -> Option<Self> {
Self::ALL.iter().copied().find(|a| a.name() == name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keybinds::{self, HORIZON_MAPPINGS, KeyBinds, MAPPINGS, all_mappings};
#[test]
fn every_mapping_is_an_action_in_order() {
let names: Vec<&str> = Action::ALL.iter().map(|a| a.name()).collect();
let mappings: Vec<&str> = MAPPINGS.iter().map(|m| m.0).collect();
assert_eq!(names[..mappings.len()], mappings[..]);
let horizon: Vec<&str> = HORIZON_MAPPINGS.iter().map(|m| m.0).collect();
let end = mappings.len() + horizon.len();
assert_eq!(names[mappings.len()..end], horizon[..]);
for action in &names[end..] {
assert!(action.starts_with("pad."), "{action}");
}
}
#[test]
fn an_actions_key_is_its_mappings_default_key() {
for (name, _, category) in all_mappings() {
let action = Action::parse(name).unwrap();
let want = if *category == "debug" {
None
} else {
keybinds::key_code(KeyBinds::default_of(name))
};
assert_eq!(action.key(), want, "{name}");
}
}
#[test]
fn every_bindable_action_is_an_action() {
for (name, _) in crate::gamepad::ACTIONS {
assert!(Action::parse(name).is_some(), "{name}");
}
}
}
+291 -35
View File
@@ -1,11 +1,17 @@
//! Controllers: Horizon's own feature (vanilla 26.3 has no gamepad input).
//! The default layout is Bedrock Edition's; see docs/controllers.md.
//! Its default buttons are Horizon's own; analog walking and toggle sneak follow Bedrock Edition on
//! consoles. See docs/controllers.md.
//!
//! This module is the backend-free part: positional buttons, the action
//! bindings, deadzones and the look curve, button edges from successive pad
//! states, and `horizon-controller.txt`, kept next to `options.txt` so that
//! file stays exactly vanilla's. The game reads the pads with gilrs.
use std::{collections::BTreeMap, fs, path::Path};
use std::{
collections::BTreeMap,
fs,
path::Path,
time::{Duration, Instant},
};
pub const FILE: &str = "horizon-controller.txt";
@@ -296,7 +302,7 @@ pub const HOTBAR_NEXT: &str = "pad.hotbarNext";
/// always pause / back and isn't listed.
pub const ACTIONS: &[(&str, Option<Button>)] = &[
("key.jump", Some(Button::South)),
("key.drop", Some(Button::East)),
("key.drop", Some(Button::DPadDown)),
("key.swapOffhand", Some(Button::West)),
("key.inventory", Some(Button::North)),
(HOTBAR_PREVIOUS, Some(Button::LeftBumper)),
@@ -305,17 +311,31 @@ pub const ACTIONS: &[(&str, Option<Button>)] = &[
("key.attack", Some(Button::RightTrigger)),
("key.sprint", Some(Button::LeftStick)),
("key.sneak", Some(Button::RightStick)),
("key.togglePerspective", Some(Button::DPadUp)),
("key.togglePerspective", Some(Button::Select)),
("key.pickItem", Some(Button::DPadLeft)),
("key.chat", Some(Button::DPadRight)),
("key.advancements", Some(Button::DPadDown)),
("key.playerlist", Some(Button::Select)),
("key.chat", Some(Button::DPadUp)),
("key.advancements", None),
("key.playerlist", None),
("key.command", None),
("key.screenshot", None),
("key.toggleGui", None),
("key.smoothCamera", None),
// Horizon's emote wheel: no button is free by default; the stick's cursor picks.
("key.emote", None),
("key.fullscreen", None),
("key.socialInteractions", None),
("key.quickActions", None),
("key.saveToolbarActivator", None),
("key.loadToolbarActivator", None),
("key.hotbar.1", None),
("key.hotbar.2", None),
("key.hotbar.3", None),
("key.hotbar.4", None),
("key.hotbar.5", None),
("key.hotbar.6", None),
("key.hotbar.7", None),
("key.hotbar.8", None),
("key.hotbar.9", None),
];
/// How a bindable action reaches the game: one of its own handlers, shared
@@ -330,6 +350,10 @@ pub enum Route {
Sneak,
Screenshot,
SmoothCamera,
Fullscreen,
/// Does nothing by itself: what reads it asks whether the button is held
/// (`Minecraft.handleKeybinds`' toolbar keys, held with a hotbar key).
Held,
Key(winit::keyboard::KeyCode),
Mouse(winit::event::MouseButton),
}
@@ -342,6 +366,8 @@ pub fn route(action: &str) -> Option<Route> {
"key.sneak" => Some(Route::Sneak),
"key.screenshot" => Some(Route::Screenshot),
"key.smoothCamera" => Some(Route::SmoothCamera),
"key.fullscreen" => Some(Route::Fullscreen),
"key.saveToolbarActivator" | "key.loadToolbarActivator" => Some(Route::Held),
_ => {
let default = crate::keybinds::KeyBinds::default_of(action);
crate::keybinds::mouse_button(default)
@@ -361,6 +387,46 @@ pub fn action_caption(action: &str) -> &str {
}
}
/// How long a held drop button throws single items before it throws the rest of the stack.
pub const DROP_STACK_HOLD: Duration = Duration::from_secs(1);
/// What a held drop button does now, past its press.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DropStep {
/// Held key.drop (a repeat reaches `KeyMapping.click` as a press).
One,
/// Control + key.drop: a pad has no Control to hold, so holding on is how it asks.
Stack,
}
/// A held drop button's repeats: a tap throws one item, as Q does; held, it repeats at the pad's
/// repeat delay and interval; and held for [`DROP_STACK_HOLD`] it throws what's left.
#[derive(Clone, Copy, Debug)]
pub struct HeldDrop {
since: Instant,
next: Instant,
}
impl HeldDrop {
pub fn new(now: Instant, delay: Duration) -> Self {
Self {
since: now,
next: now + delay,
}
}
/// The step due at `now`, if any; at most one per call.
pub fn due(&mut self, now: Instant, interval: Duration) -> Option<DropStep> {
if now < self.next {
return None;
}
self.next = now + interval;
Some(if now.duration_since(self.since) >= DROP_STACK_HOLD {
DropStep::Stack
} else {
DropStep::One
})
}
}
/// Which button pictures prompts use.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GlyphStyle {
@@ -404,13 +470,21 @@ impl GlyphStyle {
GlyphStyle::PlayStation,
GlyphStyle::Nintendo,
];
/// The style for a pad by its USB vendor (Sony, Nintendo; Microsoft,
/// Valve's Steam Deck and Steam Input's virtual pad, and everything else
/// look like Xbox).
pub fn of_vendor(vendor: Option<u16>) -> Self {
match vendor {
Some(0x054c) => GlyphStyle::PlayStation,
Some(0x057e) => GlyphStyle::Nintendo,
/// The style for a pad by its USB ids: Sony's and Nintendo's own, and a few officially licensed
/// third-party pads made for their consoles. Microsoft, Valve's Steam Deck and Steam Input's
/// virtual pad, and everything else look like Xbox.
pub fn of_device(vendor: Option<u16>, product: Option<u16>) -> Self {
match (vendor, product) {
(Some(0x054c), _) => GlyphStyle::PlayStation,
(Some(0x057e), _) => GlyphStyle::Nintendo,
// HORIPAD 4 FPS and FPS Plus; Razer Raiju; Nacon Revolution Pro.
(Some(0x0f0d), Some(0x0055 | 0x0066))
| (Some(0x1532), Some(0x1000))
| (Some(0x146b), Some(0x0d01)) => GlyphStyle::PlayStation,
// HORIPAD for Nintendo Switch; PowerA Wired Plus; PDP Faceoff Wired Pro.
(Some(0x0f0d), Some(0x00c1))
| (Some(0x20d6), Some(0xa711))
| (Some(0x0e6f), Some(0x0180)) => GlyphStyle::Nintendo,
_ => GlyphStyle::Xbox,
}
}
@@ -593,6 +667,24 @@ impl PadSettings {
}
}
}
// A file from before these defaults: its player's buttons stay where they were.
if !text.is_empty() && !text.lines().any(|line| line.starts_with("bindLayout:")) {
for (action, button) in [
("key.drop", Button::East),
("key.togglePerspective", Button::DPadUp),
("key.chat", Button::DPadRight),
("key.playerlist", Button::Select),
("key.advancements", Button::DPadDown),
] {
// A button saved by hand stays, even one that's now this action's default.
if !text
.lines()
.any(|line| line.starts_with(&format!("bind_{action}:")))
{
s.bindings.entry(action).or_insert(Some(button));
}
}
}
s
}
pub fn serialize(&self) -> String {
@@ -623,6 +715,7 @@ impl PadSettings {
button.map_or("none", Button::name)
));
}
text.push_str("bindLayout:1\n");
text
}
pub fn default_of(action: &str) -> Option<Button> {
@@ -699,6 +792,29 @@ pub fn stick_axes(
}
}
/// The tilt from which a stick holds a key in [`eight_direction_keys`]: holding one is full speed,
/// so the small tilts past the deadzone stay still.
const KEY_TILT: f32 = 0.3;
/// tan(22.5 degrees): where one of the 8 sectors meets the next.
const SECTOR_EDGE: f32 = 0.414_213_57;
/// The movement keys (forward, backward, left, right) a move vector (`[left, forward]`) holds: the
/// one of the 8 headings it is nearest, so it is at most 22.5 degrees off. An edge goes to the
/// straight heading.
pub fn eight_direction_keys([left, forward]: [f32; 2]) -> [bool; 4] {
if length([left, forward]) < KEY_TILT {
return [false; 4];
}
let along = forward.abs() > SECTOR_EDGE * left.abs();
let across = left.abs() > SECTOR_EDGE * forward.abs();
[
along && forward > 0.0,
along && forward < 0.0,
across && left > 0.0,
across && left < 0.0,
]
}
/// A radial deadzone: inside it the stick is centred; beyond it the rest of
/// the travel is rescaled to 0..1 in the same direction, so diagonals aren't
/// clipped and the first movement past the deadzone is small.
@@ -747,15 +863,6 @@ pub fn look_delta(
)
}
/// The hotbar slot after a bumper: one step with wrap-around.
pub fn hotbar_step(selected: usize, forward: bool) -> usize {
if forward {
(selected + 1) % 9
} else {
(selected + 8) % 9
}
}
/// Menu cursor speed in GUI pixels per second at full tilt.
pub const CURSOR_SPEED: f32 = 300.0;
@@ -1462,6 +1569,52 @@ mod tests {
}
}
#[test]
fn a_held_drop_repeats_then_throws_the_stack() {
let t = Instant::now();
let at = |ms: u64| t + Duration::from_millis(ms);
let (delay, interval) = (Duration::from_millis(350), Duration::from_millis(90));
let mut held = HeldDrop::new(t, delay);
assert_eq!(held.due(at(100), interval), None, "a tap is one item");
assert_eq!(held.due(at(349), interval), None);
assert_eq!(held.due(at(350), interval), Some(DropStep::One));
assert_eq!(held.due(at(400), interval), None, "not before the interval");
assert_eq!(held.due(at(440), interval), Some(DropStep::One));
assert_eq!(held.due(at(999), interval), Some(DropStep::One));
assert_eq!(held.due(at(1000), interval), None);
assert_eq!(held.due(at(1100), interval), Some(DropStep::Stack));
assert_eq!(held.due(at(1200), interval), Some(DropStep::Stack));
assert_eq!(held.due(at(1201), interval), None);
}
#[test]
fn the_later_actions_have_no_default_button_and_leave_saved_bindings() {
let defaults = PadSettings::default();
for action in [
"key.fullscreen",
"key.socialInteractions",
"key.quickActions",
"key.saveToolbarActivator",
"key.loadToolbarActivator",
"key.hotbar.1",
"key.hotbar.9",
] {
assert_eq!(defaults.binding(action), None, "{action}");
}
let read = PadSettings::parse(
"bindLayout:1
bind_key.jump:east
bind_key.hotbar.2:north
",
);
assert_eq!(read.binding("key.jump"), Some(Button::East));
assert_eq!(read.binding("key.inventory"), Some(Button::North));
assert_eq!(
read.actions_of(Button::North),
["key.inventory", "key.hotbar.2"]
);
}
#[test]
fn navigation_preferences_migrate_and_stay_with_each_player() {
let dir = std::env::temp_dir().join(format!(
@@ -1581,7 +1734,7 @@ mod tests {
}
#[test]
fn defaults_follow_bedrock() {
fn every_action_has_its_default() {
let s = PadSettings::default();
assert_eq!(s.actions_of(Button::South), ["key.jump"]);
assert_eq!(s.actions_of(Button::RightTrigger), ["key.attack"]);
@@ -1589,16 +1742,46 @@ mod tests {
assert_eq!(s.actions_of(Button::North), ["key.inventory"]);
assert_eq!(s.actions_of(Button::RightBumper), [HOTBAR_NEXT]);
assert!(s.actions_of(Button::Start).is_empty());
// Every default action names a vanilla mapping or a pad action.
// Every default action names a key mapping or a pad action.
for (action, _) in ACTIONS {
assert!(
action.starts_with("pad.")
|| crate::keybinds::MAPPINGS.iter().any(|m| m.0 == *action),
|| crate::keybinds::all_mappings().any(|m| m.0 == *action),
"{action}"
);
}
}
#[test]
fn a_file_from_before_these_defaults_keeps_its_buttons() {
// Saved settings that never bound D-pad down: it stays on the advancements.
let s = PadSettings::parse("vibration:false\nbind_key.chat:up\n");
assert_eq!(s.binding("key.advancements"), Some(Button::DPadDown));
assert_eq!(s.binding("key.chat"), Some(Button::DPadUp));
assert_eq!(s.binding("key.drop"), Some(Button::East));
assert_eq!(s.binding("key.togglePerspective"), Some(Button::DPadUp));
assert_eq!(s.binding("key.playerlist"), Some(Button::Select));
assert!(!s.conflicts("key.advancements"));
// Advancements moved by hand stays moved, and the file stays this way from then on.
let s = PadSettings::parse("bind_key.advancements:left\nbind_key.drop:down\n");
assert_eq!(s.binding("key.advancements"), Some(Button::DPadLeft));
assert_eq!(s.binding("key.drop"), Some(Button::DPadDown));
assert_eq!(PadSettings::parse(&s.serialize()), s);
}
#[test]
fn a_new_player_drops_on_d_pad_down_and_chats_on_d_pad_up() {
for s in [PadSettings::default(), PadSettings::parse("")] {
assert_eq!(s.binding("key.drop"), Some(Button::DPadDown));
assert_eq!(s.binding("key.chat"), Some(Button::DPadUp));
assert_eq!(s.binding("key.togglePerspective"), Some(Button::Select));
assert_eq!(s.binding("key.playerlist"), None);
assert_eq!(s.binding("key.advancements"), None);
}
let s = PadSettings::parse(&PadSettings::default().serialize());
assert_eq!(s, PadSettings::default());
}
#[test]
fn settings_round_trip_and_store_only_changes() {
let mut s = PadSettings::default();
@@ -1620,8 +1803,9 @@ mod tests {
assert_eq!(back.binding("key.drop"), None);
assert_eq!(back.actions_of(Button::East), ["key.jump"]);
// Garbage keeps the defaults.
let odd =
PadSettings::parse("lookSensitivity:NaN\nbind_key.nope:south\nbind_key.jump:x9\n");
let odd = PadSettings::parse(
"lookSensitivity:NaN\nbind_key.nope:south\nbind_key.jump:x9\nbindLayout:1\n",
);
assert_eq!(odd, PadSettings::default());
}
@@ -1678,6 +1862,63 @@ mod tests {
assert_eq!(Button::North.by_letter().label(GlyphStyle::Nintendo), "Y");
}
#[test]
fn a_stick_holds_the_keys_of_its_nearest_heading() {
// Forward, backward, left, right, by the heading counted from forward toward left.
let headings = [
[true, false, false, false],
[true, false, true, false],
[false, false, true, false],
[false, true, true, false],
[false, true, false, false],
[false, true, false, true],
[false, false, false, true],
[true, false, false, true],
];
for tenth in 0..3600 {
let degrees = tenth as f32 / 10.0;
if degrees % 45.0 == 22.5 {
continue;
}
for tilt in [0.31, 0.5, 1.0] {
let (s, c) = degrees.to_radians().sin_cos();
let keys = eight_direction_keys([tilt * s, tilt * c]);
let sector = ((degrees / 45.0).round() as usize) % 8;
assert_eq!(keys, headings[sector], "{degrees} at {tilt}");
let [left, forward] =
crate::input::keyboard_direction(keys[0], keys[1], keys[2], keys[3]);
let off = (left.atan2(forward).to_degrees() - degrees).rem_euclid(360.0);
assert!(off.min(360.0 - off) <= 22.5 + 1e-3, "{degrees}: {off}");
}
}
}
#[test]
fn a_sector_edge_goes_to_the_straight_heading() {
assert_eq!(
eight_direction_keys([0.414_213_57, 1.0]),
[true, false, false, false]
);
assert_eq!(
eight_direction_keys([0.4143, 1.0]),
[true, false, true, false]
);
assert_eq!(
eight_direction_keys([1.0, 0.414_213_57]),
[false, false, true, false]
);
}
#[test]
fn a_small_tilt_holds_no_key() {
assert_eq!(eight_direction_keys([0.0, 0.0]), [false; 4]);
assert_eq!(eight_direction_keys([0.2, 0.2]), [false; 4]);
assert_eq!(
eight_direction_keys([0.0, 0.31]),
[true, false, false, false]
);
}
#[test]
fn radial_deadzone_rescales() {
assert_eq!(radial([0.1, 0.05], 0.15), [0.0, 0.0]);
@@ -2208,12 +2449,27 @@ mod tests {
}
#[test]
fn hotbar_wraps_and_glyphs_follow_the_vendor() {
assert_eq!(hotbar_step(8, true), 0);
assert_eq!(hotbar_step(0, false), 8);
assert_eq!(hotbar_step(3, true), 4);
assert_eq!(GlyphStyle::of_vendor(Some(0x054c)), GlyphStyle::PlayStation);
assert_eq!(GlyphStyle::of_vendor(Some(0x28de)), GlyphStyle::Xbox);
assert_eq!(GlyphStyle::of_vendor(None), GlyphStyle::Xbox);
fn glyphs_follow_the_vendor() {
let of = |vendor, product| GlyphStyle::of_device(Some(vendor), Some(product));
// Any product of the consoles' makers.
assert_eq!(of(0x054c, 0x0ce6), GlyphStyle::PlayStation);
assert_eq!(of(0x054c, 0x0001), GlyphStyle::PlayStation);
assert_eq!(of(0x057e, 0x2009), GlyphStyle::Nintendo);
// A pad made for a console by another maker, and that maker's others.
assert_eq!(of(0x0f0d, 0x0066), GlyphStyle::PlayStation);
assert_eq!(of(0x0f0d, 0x00c1), GlyphStyle::Nintendo);
assert_eq!(of(0x0f0d, 0x0067), GlyphStyle::Xbox);
assert_eq!(of(0x20d6, 0xa711), GlyphStyle::Nintendo);
// Valve's virtual pad, Microsoft's, nothing known, nothing reported.
assert_eq!(of(0x28de, 0x11ff), GlyphStyle::Xbox);
assert_eq!(of(0x045e, 0x0b12), GlyphStyle::Xbox);
assert_eq!(of(0x1234, 0x5678), GlyphStyle::Xbox);
assert_eq!(GlyphStyle::of_device(None, None), GlyphStyle::Xbox);
// A vendor alone is enough only for the consoles' makers.
assert_eq!(
GlyphStyle::of_device(Some(0x054c), None),
GlyphStyle::PlayStation
);
assert_eq!(GlyphStyle::of_device(Some(0x0f0d), None), GlyphStyle::Xbox);
}
}
+32 -5
View File
@@ -3,15 +3,18 @@
//! device letting go (splitscreen moving the keyboard to another player, say)
//! can't release what another still presses.
use crate::actions::Action;
use std::collections::HashMap;
use winit::keyboard::KeyCode;
/// Where a press came from.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Source {
/// The keyboard and mouse, and the scripting socket acting as them.
/// The keyboard and mouse.
Keyboard,
Pad,
/// `horizon-ctl`'s `key` and `mouse` and the agent commands that walk and click.
Agent,
}
impl Source {
@@ -19,6 +22,7 @@ impl Source {
match self {
Source::Keyboard => 1,
Source::Pad => 2,
Source::Agent => 4,
}
}
}
@@ -29,9 +33,20 @@ pub struct HeldKeys {
}
impl HeldKeys {
pub fn contains(&self, key: &KeyCode) -> bool {
/// Whether anything holds this mapping: every device, whatever the mapping is bound to.
pub fn held(&self, action: Action) -> bool {
action.key().is_some_and(|key| self.raw(&key))
}
/// Whether a key that is no mapping's default is held (Right Shift, the arrows, Alt, the system
/// key), or the key a caller already holds as a key: most code wants [`HeldKeys::held`].
pub fn raw(&self, key: &KeyCode) -> bool {
self.held.contains_key(key)
}
/// The old way of asking, by a mapping's default key; clippy refuses it (`clippy.toml`). It
/// stays so that a change written against it says what to use instead.
pub fn contains(&self, key: &KeyCode) -> bool {
self.raw(key)
}
/// `source` holds `key`. True when nothing held it before.
pub fn press(&mut self, key: KeyCode, source: Source) -> bool {
let sources = self.held.entry(key).or_insert(0);
@@ -72,7 +87,7 @@ impl HeldKeys {
/// Held, by the keyboard unless a device already holds it: for callers
/// that don't say where the press came from.
pub fn insert(&mut self, key: KeyCode) -> bool {
if self.contains(&key) {
if self.raw(&key) {
return false;
}
self.press(key, Source::Keyboard)
@@ -132,13 +147,25 @@ mod tests {
assert!(!keys.press(KeyCode::Space, Source::Keyboard));
assert!(keys.press(KeyCode::KeyW, Source::Keyboard));
assert_eq!(keys.release_source(Source::Keyboard), vec![KeyCode::KeyW]);
assert!(keys.contains(&KeyCode::Space) && !keys.contains(&KeyCode::KeyW));
assert!(keys.held(Action::Jump) && !keys.held(Action::Forward));
assert!(keys.held_by(&KeyCode::Space, Source::Pad));
assert!(!keys.held_by(&KeyCode::Space, Source::Keyboard));
assert!(keys.release(&KeyCode::Space, Source::Pad));
assert!(keys.is_empty());
}
#[test]
fn a_script_holds_a_key_by_itself() {
let mut keys = HeldKeys::default();
keys.press(KeyCode::KeyW, Source::Agent);
keys.press(KeyCode::KeyW, Source::Keyboard);
assert!(!keys.release(&KeyCode::KeyW, Source::Keyboard));
assert!(keys.held(Action::Forward));
assert!(keys.release_source(Source::Pad).is_empty());
assert_eq!(keys.release_source(Source::Agent), vec![KeyCode::KeyW]);
assert!(!keys.held(Action::Forward));
}
#[test]
fn untagged_inserts_and_removes_act_as_before() {
let mut keys = HeldKeys::default();
@@ -149,7 +176,7 @@ mod tests {
keys.remove(&KeyCode::ShiftLeft),
"remove lets go for everyone"
);
assert!(!keys.contains(&KeyCode::ShiftLeft));
assert!(!keys.held(Action::Sneak));
assert!(keys.release(&KeyCode::KeyA, Source::Keyboard), "never held");
}
}
@@ -51,11 +51,6 @@ impl InteractionInput {
pub fn press(&mut self, button: Button) {
self.clicks[button as usize] += 1;
}
/// Whether a click waits for the next tick.
pub fn has_clicks(&self) -> bool {
self.clicks.iter().any(|&c| c > 0)
}
pub fn tick(&mut self, world: &mut impl InteractionWorld, attack_down: bool, use_down: bool) {
let [attack, use_item, pick] = std::mem::take(&mut self.clicks);
world.pick_hit_result();
+59 -16
View File
@@ -23,8 +23,6 @@ pub const MAPPINGS: &[(&str, &str, &str)] = &[
("key.pickItem", "key.mouse.middle", "gameplay"),
("key.command", "key.keyboard.slash", "multiplayer"),
("key.friends", "key.keyboard.o", "multiplayer"),
// Horizon's emote wheel (docs/cosmetics.md), on a key vanilla leaves free.
("key.emote", "key.keyboard.b", "multiplayer"),
("key.socialInteractions", "key.keyboard.p", "multiplayer"),
("key.toggleGui", "key.keyboard.f1", "misc"),
(
@@ -76,6 +74,17 @@ pub const MAPPINGS: &[(&str, &str, &str)] = &[
("key.debug.lightmapTexture", "key.keyboard.4", "debug"),
("key.debug.improvedTransparency", "key.keyboard.x", "debug"),
];
/// Horizon's own key mappings: bound in Controls like the vanilla ones, but kept in
/// `horizon-extras.txt` (`emoteKey`), never in `options.txt`, which stays exactly vanilla's
/// (`MAPPINGS` is what the options parity harness compares with the pinned client).
pub const HORIZON_MAPPINGS: &[(&str, &str, &str)] = &[
// The emote wheel (docs/cosmetics.md), on a key vanilla leaves free.
("key.emote", "key.keyboard.b", "multiplayer"),
];
/// Every mapping the game binds: vanilla's in `Options.keyMappings` order, then Horizon's.
pub fn all_mappings() -> impl Iterator<Item = &'static (&'static str, &'static str, &'static str)> {
MAPPINGS.iter().chain(HORIZON_MAPPINGS)
}
/// Mappings Options builds as `ToggleKeyMapping` (hold or toggle by option).
pub fn is_toggle(name: &str) -> bool {
matches!(name, "key.sneak" | "key.sprint" | "key.use" | "key.attack")
@@ -314,8 +323,7 @@ pub struct KeyBinds {
impl Default for KeyBinds {
fn default() -> Self {
Self {
keys: MAPPINGS
.iter()
keys: all_mappings()
.map(|(n, d, _)| (*n, (*d).to_owned()))
.collect(),
}
@@ -341,8 +349,7 @@ impl KeyBinds {
.map_or("key.keyboard.unknown", String::as_str)
}
pub fn default_of(name: &str) -> &'static str {
MAPPINGS
.iter()
all_mappings()
.find(|m| m.0 == name)
.map_or("key.keyboard.unknown", |m| m.1)
}
@@ -350,16 +357,24 @@ impl KeyBinds {
self.get(name) == Self::default_of(name)
}
pub fn set(&mut self, name: &str, key: &str) {
if let Some((n, _, _)) = MAPPINGS.iter().find(|m| m.0 == name) {
if let Some((n, _, _)) = all_mappings().find(|m| m.0 == name) {
self.keys.insert(n, key.to_owned());
}
}
/// The bindings `options.txt` holds: vanilla's mappings only.
pub fn saved(&self) -> Vec<(&'static str, String)> {
MAPPINGS
.iter()
.map(|(n, _, _)| (*n, self.get(n).to_owned()))
.collect()
}
/// Horizon's own bindings, for `horizon-extras.txt`.
pub fn horizon_saved(&self) -> Vec<(&'static str, String)> {
HORIZON_MAPPINGS
.iter()
.map(|(n, _, _)| (*n, self.get(n).to_owned()))
.collect()
}
pub fn reset_all(&mut self) {
*self = Self::default();
}
@@ -369,8 +384,7 @@ impl KeyBinds {
if key == "key.keyboard.unknown" {
return Vec::new();
}
MAPPINGS
.iter()
all_mappings()
.filter(|(n, _, _)| {
*n != name && self.get(n) == key && (!self.is_default(n) || !self.is_default(name))
})
@@ -384,8 +398,7 @@ impl KeyBinds {
/// Every mapping a physical input triggers, in list order: like
/// `KeyMapping.set`/`click`, one key drives all the mappings bound to it.
pub fn mappings_for<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
all_mappings()
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
}
@@ -395,8 +408,7 @@ impl KeyBinds {
/// key and the binding itself); their defaults (A, S, D, T, L, 1 to 4...)
/// would otherwise turn the key back into its old gameplay meaning.
fn translated_mappings<'a>(&'a self, key: &'a str) -> impl Iterator<Item = &'static str> + 'a {
MAPPINGS
.iter()
all_mappings()
.filter(|m| m.2 != "debug")
.map(|m| m.0)
.filter(move |n| self.get(n) == key)
@@ -404,9 +416,7 @@ impl KeyBinds {
/// Whether a mapping (other than a debug key) whose default is `key` was
/// bound elsewhere.
fn default_moved(&self, key: &str) -> bool {
MAPPINGS
.iter()
.any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
all_mappings().any(|(n, d, c)| *d == key && *c != "debug" && !self.is_default(n))
}
/// Translate a physical key into the default key of the mapping it is
/// bound to, so game code written against vanilla defaults follows the
@@ -442,6 +452,18 @@ impl KeyBinds {
}
vec![Ok(key)]
}
/// `Minecraft.handleKeybinds`: key.pickItem and key.spectatorHotbar share the middle button by
/// default and are separate mappings, so a press of what `input` is bound to acts only as the
/// one this player can use: the spectator hotbar for a spectator, pick block otherwise. (Pick
/// block does nothing a spectator would see.)
pub fn middle_acts(&self, input: &str, spectator: bool) -> bool {
let mapping = if spectator {
"key.spectatorHotbar"
} else {
"key.pickItem"
};
self.get(mapping) == input
}
/// Translate a mouse button the same way (keys bound to mouse buttons).
pub fn translate_mouse(&self, button: MouseButton) -> Option<Result<KeyCode, MouseButton>> {
self.translate_mouse_all(button).into_iter().next()
@@ -505,6 +527,27 @@ impl KeyBinds {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mappings_sharing_the_middle_button_act_as_themselves() {
let middle = "key.mouse.middle";
let z = "key.keyboard.z";
let mut keys = KeyBinds::default();
assert!(keys.middle_acts(middle, false) && keys.middle_acts(middle, true));
// The spectator hotbar on Z: the middle button only picks, and Z never picks.
keys.set("key.spectatorHotbar", z);
assert!(keys.middle_acts(middle, false));
assert!(!keys.middle_acts(middle, true));
assert!(keys.middle_acts(z, true));
assert!(!keys.middle_acts(z, false));
// Pick block on Z instead: the other way round.
keys.reset_all();
keys.set("key.pickItem", z);
assert!(keys.middle_acts(z, false));
assert!(!keys.middle_acts(z, true));
assert!(keys.middle_acts(middle, true));
assert!(!keys.middle_acts(middle, false));
}
#[test]
fn container_mouse_hotkeys_follow_slot_bindings_and_pick_item_priority() {
for button in [MouseButton::Back, MouseButton::Forward, MouseButton::Middle] {
+1
View File
@@ -3,6 +3,7 @@
//! options. horizon-client re-exports each module under its old path
//! (`horizon_client::keybinds`, ...).
#![forbid(unsafe_code)]
pub mod actions;
pub mod clipboard;
pub mod gamepad;
pub mod held;
+3 -12
View File
@@ -6,13 +6,7 @@ use horizon_world::{
worldgen::{tags::TagDefinitions, terrain::TerrainBlockStates},
};
use serde_json::Value;
use std::{
collections::BTreeSet,
fs::File,
io::{self, BufReader},
path::Path,
sync::Arc,
};
use std::{collections::BTreeSet, io, path::Path, sync::Arc};
pub const MATERIALS: [&str; 6] = ["bedrock", "dirt", "stone", "sand", "oak_planks", "bricks"];
#[derive(Clone, Copy)]
@@ -190,15 +184,12 @@ fn invalid(message: &str) -> io::Error {
}
impl Assets {
pub fn load(root: &Path) -> Result<Self> {
let manifest: Value = serde_json::from_reader(BufReader::new(File::open(
root.join("horizon-resources.json"),
)?))?;
let manifest: Value = horizon_world::json_file::read(&root.join("horizon-resources.json"))?;
if manifest["data_version"].as_i64() != Some(i64::from(DATA_VERSION)) {
return Err(invalid("resources do not match Horizon's pinned data version").into());
}
let (registry, flags) = horizon_world::level::data::load_registry_with_flags(root)?;
let bundle: Value =
serde_json::from_reader(BufReader::new(File::open(root.join("block-tags.json"))?))?;
let bundle: Value = horizon_world::json_file::read(&root.join("block-tags.json"))?;
let mut definitions = TagDefinitions::default();
for (name, text) in bundle
.as_object()
+1 -1
View File
@@ -288,7 +288,7 @@ impl Biomes {
.collect();
files.sort_by_cached_key(|f| format!("{}.json", f.name));
for file in files {
let v: Value = serde_json::from_slice(file.bytes()?)?;
let v: Value = horizon_world::json_file::parse(&file.path, file.bytes()?)?;
list.push(BiomeInfo::from_json(
format!("minecraft:{}", file.name),
&v,
+35
View File
@@ -225,6 +225,8 @@ pub const BIOME_STRATEGY: Strategy = Strategy::biomes(8);
/// (what vanilla servers and clients send), more when content mods add states
/// past 65,536 (docs/modpacks.md). Set once, before the first chunk.
static BLOCK_BITS: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(16);
/// States in the registry, set with [`BLOCK_BITS`].
static BLOCK_STATES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(u32::MAX);
/// Block states are `PalettedContainer`s over the registry: vanilla's 16-bit
/// global palette, wider with enough content mods.
@@ -232,6 +234,35 @@ pub fn block_strategy() -> Strategy {
Strategy::blocks(BLOCK_BITS.load(std::sync::atomic::Ordering::Relaxed))
}
/// Whether the registry has this state. Every id is known until the registry is loaded.
pub fn known_block_state(id: u32) -> bool {
id < BLOCK_STATES.load(std::sync::atomic::Ordering::Relaxed)
}
/// A server's block container in our palette width. Ids past our registry (a
/// server or host with mods we lack) read as air: every per-state table is
/// indexed by them, and a palette's unused entries are scanned too.
pub fn known_blocks_only(blocks: PalettedContainer) -> PalettedContainer {
let ours = block_strategy();
let unknown = |id: u32| !known_block_state(id) || u64::from(id) >> ours.global_bits != 0;
let any_unknown = if blocks.is_global() {
blocks.has(unknown)
} else {
blocks.maybe_has(unknown)
};
if !any_unknown && blocks.strategy().global_bits == ours.global_bits {
return blocks;
}
let mut values = [0u32; 4096];
blocks.unpack(&mut values);
for value in &mut values {
if unknown(*value) {
*value = 0;
}
}
PalettedContainer::from_values(ours, &values)
}
/// Frees `columns` (`count` of them) on a short-lived thread of its own
/// when there are many: a teleport or a dimension change drops a few
/// thousand columns at once, and freeing their sections took the frame
@@ -250,6 +281,10 @@ pub fn free_columns<T: Send + 'static>(columns: T, count: usize) {
pub fn set_block_states(states: usize) {
let bits = horizon_core::paletted::ceil_log2(states).max(16);
BLOCK_BITS.store(bits, std::sync::atomic::Ordering::Relaxed);
BLOCK_STATES.store(
u32::try_from(states).unwrap_or(u32::MAX),
std::sync::atomic::Ordering::Relaxed,
);
forget_shared();
}
@@ -133,6 +133,13 @@ pub fn rows() -> Vec<(Page, Vec<(&'static str, Vec<Setting>)>)> {
),
],
),
(
"Play",
vec![option(
Opt::PadTriggerThreshold,
"How far a trigger is pulled before it counts as pressed.",
)],
),
(
"Menus",
vec![
@@ -1266,6 +1273,7 @@ mod tests {
Opt::PadInvertY,
Opt::PadMoveDeadzone,
Opt::PadLookDeadzone,
Opt::PadTriggerThreshold,
Opt::PadGlyphs,
Opt::PadKeyboard,
Opt::PadPointerSpeed,
@@ -3,7 +3,7 @@
//! celestial angles and star brightness for the overworld day cycle.
use crate::Result;
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};
use std::{collections::HashMap, path::Path};
#[derive(Clone, Copy, Debug)]
enum Easing {
@@ -151,13 +151,13 @@ impl Environment {
/// A dimension's base attributes, optionally driven by the day timeline.
pub fn load_dimension(root: &Path, dimension: &str, day_cycle: bool) -> Result<Self> {
let timeline: Value = if day_cycle {
serde_json::from_slice(&fs::read(root.join("data/minecraft/timeline/day.json"))?)?
horizon_world::json_file::read(&root.join("data/minecraft/timeline/day.json"))?
} else {
serde_json::json!({"period_ticks": 24000, "tracks": {}})
};
let dimension: Value = serde_json::from_slice(&fs::read(
root.join(format!("data/minecraft/dimension_type/{dimension}.json")),
)?)?;
let dimension: Value = horizon_world::json_file::read(
&root.join(format!("data/minecraft/dimension_type/{dimension}.json")),
)?;
let mut tracks = HashMap::new();
for (name, track) in timeline["tracks"].as_object().ok_or("invalid timeline")? {
let easing = match &track["ease"] {
+66 -28
View File
@@ -10,6 +10,9 @@ pub const FILE: &str = "horizon-extras.txt";
pub const SORT_KEY: &str = "horizon.sort";
/// The 3D skin layers line, here and in the `options.txt` that held it first.
const SKIN_LAYERS_KEY: &str = "threeDSkinLayers";
/// The emote wheel's key, here and in the `key_key.emote` line #10092 wrote to `options.txt`.
const EMOTE_KEY: &str = "emoteKey";
const OLD_EMOTE_LINE: &str = "key_key.emote";
/// The minimap's sizes in GUI pixels, and its zooms in blocks per pixel.
pub const MINIMAP_SIZES: [(u8, &str); 3] = [(64, "Small"), (96, "Medium"), (128, "Large")];
@@ -126,6 +129,10 @@ pub struct Extras {
pub sort: bool,
/// A vanilla key name: sorts the part under the cursor.
pub sort_key: String,
/// The emote wheel's key (`key.emote`), as the game last saved it. The live binding is
/// `Settings::keys`, bound in Controls with the vanilla ones; it is only kept in this file
/// so that `options.txt` stays vanilla's (`Settings::load` hands it over, `save` takes it back).
pub emote_key: String,
/// A new chat line slides in from the left while it fades in (`Chat::layout`).
pub chat_animation: bool,
/// The indicator bar at the left of a chat line (on by default, as vanilla).
@@ -164,10 +171,10 @@ pub struct Extras {
/// Cosmetics (`horizon_cosmetics`, off unless enabled), the lines `horizon_cosmetics::prefs`
/// shares with the launcher's Wardrobe: Show my cosmetics (the account's, sent to the service;
/// on by default), how much of other players' to show (no line until chosen:
/// `effective_others`), and their auras and trails (off by default).
/// `effective_others`), and whether their trails and auras are hidden (off by default).
pub cosmetics_mine: bool,
pub cosmetics_others: Option<horizon_cosmetics::prefs::Others>,
pub cosmetics_auras: bool,
pub cosmetics_hide_fx: bool,
/// My cosmetics in single player too (`cosmeticsSolo`, off by default): the window's own server
/// names the player by their offline profile, which the service doesn't know, so their own
/// cosmetics and name tag style show only with this on.
@@ -220,6 +227,7 @@ impl Default for Extras {
mouse_tweaks_wheel: true,
sort: false,
sort_key: "key.keyboard.unknown".into(),
emote_key: crate::keybinds::KeyBinds::default_of("key.emote").into(),
chat_animation: false,
chat_indicator: true,
no_ai: false,
@@ -230,12 +238,12 @@ impl Default for Extras {
splitscreen_two: 0,
splitscreen_three: 0,
splitscreen_names: true,
wheel_cycles_settings: true,
wheel_cycles_settings: false,
music_off: Vec::new(),
music_page: false,
cosmetics_mine: true,
cosmetics_others: None,
cosmetics_auras: false,
cosmetics_hide_fx: false,
cosmetics_solo: false,
cosmetics_owner: false,
horizon_marks: true,
@@ -324,6 +332,7 @@ impl Extras {
"mouseTweaksWheel" => e.mouse_tweaks_wheel = flag(e.mouse_tweaks_wheel),
"sort" => e.sort = flag(e.sort),
"sortKey" if value.starts_with("key.") => e.sort_key = value.to_owned(),
EMOTE_KEY if value.starts_with("key.") => e.emote_key = value.to_owned(),
"chatAnimation" => e.chat_animation = flag(e.chat_animation),
"chatIndicator" => e.chat_indicator = flag(e.chat_indicator),
horizon_core::no_ai::KEY => e.no_ai = flag(e.no_ai),
@@ -334,7 +343,11 @@ impl Extras {
"splitscreenTwo" => e.splitscreen_two = value.parse().unwrap_or(0).min(2),
"splitscreenThree" => e.splitscreen_three = value.parse().unwrap_or(0).min(2),
"splitscreenNames" => e.splitscreen_names = flag(e.splitscreen_names),
"wheelCyclesSettings" => e.wheel_cycles_settings = flag(e.wheel_cycles_settings),
// A new name: `wheelCyclesSettings` was written as true by every
// install while it was the default, so it says nothing of a choice.
"wheelCyclesListSettings" => {
e.wheel_cycles_settings = flag(e.wheel_cycles_settings)
}
"musicOff" => {
e.music_off = value
.split(',')
@@ -349,7 +362,7 @@ impl Extras {
e.cosmetics_others =
horizon_cosmetics::prefs::Others::parse(value).or(e.cosmetics_others)
}
"cosmeticsAuras" => e.cosmetics_auras = flag(e.cosmetics_auras),
"cosmeticsHideFx" => e.cosmetics_hide_fx = flag(e.cosmetics_hide_fx),
"cosmeticsSolo" => e.cosmetics_solo = flag(e.cosmetics_solo),
horizon_cosmetics::prefs::MARKS => e.horizon_marks = flag(e.horizon_marks),
horizon_cosmetics::prefs::LISTED => e.horizon_listed = flag(e.horizon_listed),
@@ -363,7 +376,7 @@ impl Extras {
}
pub fn serialize(&self) -> String {
let mut text = format!(
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsAuras:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
"packBrowser:{}\npregen:{}\nfullbright:{}\ndynamicLights:{}\nzoom:{}\nzoomKey:{}\nzoomLevel:{}\nzoomSmooth:{}\nzoomSensitivity:{}\nlodZoomDetail:{}\nfps:{}\nfpsCorner:{}\nfpsLows:{}\nkeystrokes:{}\nkeystrokesCorner:{}\nkeystrokesCps:{}\nmovedToMods:{}\nminimap:{}\nminimapCorner:{}\nminimapSize:{}\nminimapShape:{}\nminimapZoom:{}\nminimapRotate:{}\nautoReconnect:{}\nuiSoundReserve:{}\nownNameTag:{}\nhideBlockParticles:{}\nmouseTweaks:{}\nmouseTweaksWheel:{}\nsort:{}\nsortKey:{}\nchatAnimation:{}\nchatIndicator:{}\n{}:{}\nagentControl:{}\nreproKeys:{}\ndiscordPresence:{}\nsplitscreen:{}\nsplitscreenTwo:{}\nsplitscreenThree:{}\nsplitscreenNames:{}\nwheelCyclesListSettings:{}\nmusicOff:{}\nmusicPage:{}\ncosmeticsMine:{}\n{}cosmeticsHideFx:{}\ncosmeticsSolo:{}\n{}:{}\n{}:{}\n{}:{}\n",
bool_text(self.pack_browser),
bool_text(self.pregen),
bool_text(self.fullbright),
@@ -417,7 +430,7 @@ impl Extras {
// Written once the player chose.
self.cosmetics_others
.map_or(String::new(), |o| format!("cosmeticsOthers:{}\n", o.name())),
bool_text(self.cosmetics_auras),
bool_text(self.cosmetics_hide_fx),
bool_text(self.cosmetics_solo),
horizon_cosmetics::prefs::MARKS,
bool_text(self.horizon_marks),
@@ -426,32 +439,44 @@ impl Extras {
SKIN_LAYERS_KEY,
bool_text(self.three_d_skin_layers),
);
text.push_str(&format!("{EMOTE_KEY}:{}\n", self.emote_key));
self.pvp.serialize(&mut text);
text
}
/// Adopts the settings `options.txt` held before they moved here, while
/// this file (`own`) has no line of its own: 3D skin layers was an
/// `options.txt` line. Vanilla's `Options.save` drops lines it doesn't
/// this file (`own`) has no line of its own: 3D skin layers and the emote key were
/// `options.txt` lines. Vanilla's `Options.save` drops lines it doesn't
/// know, so it is read there once, the first start after the update.
pub fn read_old_options(&mut self, own: &str, options_txt: &str) {
fn line(text: &str) -> Option<&str> {
fn line<'a>(text: &'a str, name: &str) -> Option<&'a str> {
text.lines().find_map(|l| {
l.split_once(':')
.filter(|(key, _)| *key == SKIN_LAYERS_KEY)
.filter(|(key, _)| *key == name)
.map(|(_, value)| value)
})
}
if line(own).is_none()
&& let Some(value) = line(options_txt)
if line(own, SKIN_LAYERS_KEY).is_none()
&& let Some(value) = line(options_txt, SKIN_LAYERS_KEY)
{
self.three_d_skin_layers = value == "true";
}
// The emote key was a `key_key.emote` line of options.txt (#10092): a rebound key is kept.
if line(own, EMOTE_KEY).is_none()
&& let Some(value) = line(options_txt, OLD_EMOTE_LINE).map(str::trim)
&& value.starts_with("key.")
&& (value == "key.keyboard.unknown"
|| crate::keybinds::key_code(value).is_some()
|| crate::keybinds::mouse_button(value).is_some())
{
self.emote_key = value.to_owned();
}
}
/// Binds the extra key `name` (`SORT_KEY`); false for
/// another name (a vanilla mapping).
pub fn set_key(&mut self, name: &str, value: String) -> bool {
match name {
SORT_KEY => self.sort_key = value,
crate::pvp::HUD_EDITOR_KEY => self.pvp.editor_key = value,
_ => return false,
}
true
@@ -598,7 +623,7 @@ pub enum Item {
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
CosmeticsMine,
CosmeticsOthers,
CosmeticsAuras,
CosmeticsHideFx,
CosmeticsSolo,
HorizonMarks,
HorizonMe,
@@ -638,7 +663,7 @@ pub fn rows() -> Vec<Result<Item, &'static str>> {
Ok(CosmeticsMine),
Ok(CosmeticsSolo),
Ok(CosmeticsOthers),
Ok(CosmeticsAuras),
Ok(CosmeticsHideFx),
Ok(HorizonMarks),
Ok(HorizonMe),
]);
@@ -672,7 +697,7 @@ impl Item {
Item::Pregen => "horizon.extras.pregen",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others",
Item::CosmeticsAuras => "horizon.extras.cosmetics_auras",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo",
Item::HorizonMarks => "horizon.extras.horizon_marks",
Item::HorizonMe => "horizon.extras.horizon_me",
@@ -700,12 +725,12 @@ impl Item {
Item::Splitscreen => "horizon.extras.splitscreen.tooltip",
Item::ChatAnimation => "horizon.extras.chat_animation.tooltip",
Item::ChatIndicator => "horizon.extras.chat_indicator.tooltip",
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_settings.tooltip",
Item::WheelCyclesSettings => "horizon.extras.wheel_cycles_list_settings.tooltip",
Item::PackBrowser => "horizon.extras.pack_browser.tooltip",
Item::Pregen => "horizon.extras.pregen.tooltip",
Item::CosmeticsMine => "horizon.extras.cosmetics_mine.tooltip",
Item::CosmeticsOthers => "horizon.extras.cosmetics_others.tooltip",
Item::CosmeticsAuras => "horizon.extras.cosmetics_auras.tooltip",
Item::CosmeticsHideFx => "horizon.extras.cosmetics_hide_fx.tooltip",
Item::CosmeticsSolo => "horizon.extras.cosmetics_solo.tooltip",
Item::HorizonMarks => "horizon.extras.horizon_marks.tooltip",
Item::HorizonMe => "horizon.extras.horizon_me.tooltip",
@@ -747,7 +772,7 @@ impl Item {
Item::Pregen => "Chunk pregeneration",
Item::CosmeticsMine => "My cosmetics",
Item::CosmeticsOthers => "Other players' cosmetics",
Item::CosmeticsAuras => "Others' auras and trails",
Item::CosmeticsHideFx => "Hide other players' trails and auras",
Item::CosmeticsSolo => "In single player",
Item::HorizonMarks => "Horizon logos",
Item::HorizonMe => "Show me as Horizon",
@@ -766,7 +791,7 @@ impl Item {
Fullbright | DynamicLights | OwnNameTag | HideBlockParticles | MouseTweaks | Sort
| AutoReconnect | NoAi | AgentControl | ReproKeys | DiscordPresence | Splitscreen
| ChatAnimation | WheelCyclesSettings | MusicPage | PackBrowser | CosmeticsMine
| CosmeticsOthers | CosmeticsAuras | CosmeticsSolo | HorizonMarks | HorizonMe
| CosmeticsOthers | CosmeticsHideFx | CosmeticsSolo | HorizonMarks | HorizonMe
| Pregen => {
return None;
}
@@ -834,7 +859,9 @@ impl Item {
Item::CosmeticsOthers => {
"What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear."
}
Item::CosmeticsAuras => "Auras and trails other players wear. Off by default.",
Item::CosmeticsHideFx => {
"Other players' trails and auras stay hidden; their other cosmetics still show. Off by default."
}
Item::CosmeticsSolo => {
"Show your cosmetics and your name tag's badge in your single-player worlds too (your name tag with Own name tag). Off: only on servers."
}
@@ -872,7 +899,7 @@ impl Item {
| Item::PackBrowser => Some(Cost::None),
Item::CosmeticsMine
| Item::CosmeticsOthers
| Item::CosmeticsAuras
| Item::CosmeticsHideFx
| Item::CosmeticsSolo
| Item::HorizonMarks
| Item::HorizonMe => Some(Cost::Low),
@@ -902,7 +929,7 @@ impl Item {
Item::WheelCyclesSettings => e.wheel_cycles_settings,
Item::PackBrowser => e.pack_browser,
Item::Pregen => e.pregen,
Item::CosmeticsAuras => e.cosmetics_auras,
Item::CosmeticsHideFx => e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks,
Item::HorizonMe => e.horizon_listed,
@@ -967,7 +994,7 @@ impl Item {
let o = e.effective_others();
e.cosmetics_others = Some(if back { o.next().next() } else { o.next() });
}
Item::CosmeticsAuras => e.cosmetics_auras = !e.cosmetics_auras,
Item::CosmeticsHideFx => e.cosmetics_hide_fx = !e.cosmetics_hide_fx,
Item::CosmeticsSolo => e.cosmetics_solo = !e.cosmetics_solo,
Item::HorizonMarks => e.horizon_marks = !e.horizon_marks,
Item::HorizonMe => e.horizon_listed = !e.horizon_listed,
@@ -979,6 +1006,7 @@ impl Item {
pub fn key_name(self) -> Option<&'static str> {
match self {
Item::SortKey => Some(SORT_KEY),
Item::Pvp(crate::pvp::Opt::HudEditorKey) => Some(crate::pvp::HUD_EDITOR_KEY),
_ => None,
}
}
@@ -996,7 +1024,7 @@ mod tests {
..Extras::default()
};
assert_eq!(
(e.effective_others(), e.cosmetics_auras, e.cosmetics_mine),
(e.effective_others(), e.cosmetics_hide_fx, e.cosmetics_mine),
(Others::Shown, false, true)
);
Item::CosmeticsOthers.cycle(&mut e, false);
@@ -1055,6 +1083,16 @@ mod tests {
assert_eq!(hidden.effective_others(), Others::Hidden);
}
#[test]
fn a_saved_wheel_setting_from_before_the_default_changed_is_dropped() {
assert!(!Extras::parse("wheelCyclesSettings:true\n").wheel_cycles_settings);
let on = Extras {
wheel_cycles_settings: true,
..Extras::default()
};
assert!(Extras::parse(&on.serialize()).wheel_cycles_settings);
}
#[test]
fn everything_is_off_by_default_and_round_trips() {
let d = Extras::default();
@@ -1063,7 +1101,7 @@ mod tests {
assert!(!d.mouse_tweaks && !d.sort && !d.own_name_tag && !d.ui_sound_reserve);
assert!(!d.hide_block_particles && !d.pregen);
assert!(!d.music_page && d.music_off.is_empty());
assert!(d.discord_presence && d.wheel_cycles_settings);
assert!(d.discord_presence && !d.wheel_cycles_settings);
assert_eq!(Extras::parse(""), d);
assert_eq!(Extras::parse(&d.serialize()), d);
let mut e = d.clone();
@@ -1107,7 +1145,7 @@ mod tests {
Item::Splitscreen.cycle(&mut e, false);
assert_eq!(Item::Splitscreen.switch(&e), Some(true));
Item::WheelCyclesSettings.cycle(&mut e, false);
assert_eq!(Item::WheelCyclesSettings.switch(&e), Some(false));
assert_eq!(Item::WheelCyclesSettings.switch(&e), Some(true));
e.music_off = vec![
"minecraft:music/game/calm1".into(),
"minecraft:music.menu".into(),
+124 -3
View File
@@ -5795,6 +5795,31 @@ impl Renderer {
blocks.iter().map(move |block| (block, fully_inside))
})
}
/// The block entities of the sections a shader pack's shadow pass drew in the last frame
/// (`shadow_visible`), within the 64 blocks `visible_block_entities_in_frustum` keeps.
/// Our probe packs' Iris 1.11.7 shadow maps hold a block entity wherever the shadow pass
/// draws its section's terrain, whether or not the camera's view takes it in (#8464).
pub fn shadow_pass_block_entities(
&self,
camera: DVec3,
) -> impl Iterator<Item = &([i32; 3], u32)> + '_ {
block_entities_within_reach(
self.shadow_visible.iter().map(|v| v.key),
&self.block_entities,
camera,
self.layout.min_y(),
)
}
/// Whether a shader pack's shadow pass drew the section holding block `pos` in the last
/// frame (`shadow_pass_block_entities`).
pub fn shadow_pass_drew(&self, pos: [i32; 3]) -> bool {
let key = (
pos[0] >> 4,
(pos[1] - self.layout.min_y()) >> 4,
pos[2] >> 4,
);
self.shadow_visible.iter().any(|v| v.key == key)
}
/// Milliseconds since `key` was uploaded; a section not in `section_uploaded` is old.
fn section_elapsed_ms(&self, key: SectionKey, now: std::time::Instant) -> u64 {
self.section_uploaded.get(&key).map_or(u64::MAX, |t| {
@@ -7636,9 +7661,7 @@ impl Renderer {
if let Some(v) = &shadow_view {
let radius = (frame.detail_distance / 16.0) as i32;
let light = ShadowCull {
light: Frustum {
planes: frustum(v.cull),
},
light: shadow_box(v),
casters: v.view_casters.then(|| shadow_casters(cull_proj, v.light)),
reach: v.distance,
};
@@ -11441,6 +11464,39 @@ impl Cull for ShadowCull {
self.casters.is_none()
}
}
/// The block entities of `sections` whose box, a block wider on every side, comes within the
/// 64 blocks block entities are drawn to (`Renderer::visible_block_entities_in_frustum`).
fn block_entities_within_reach<'a>(
sections: impl Iterator<Item = SectionKey> + 'a,
block_entities: &'a FxHashMap<SectionKey, Vec<([i32; 3], u32)>>,
camera: DVec3,
min_y: i32,
) -> impl Iterator<Item = &'a ([i32; 3], u32)> + 'a {
sections
.filter_map(move |key| {
let min = (DVec3::new(
f64::from(key.0 * 16 - 1),
f64::from(min_y + key.1 * 16 - 1),
f64::from(key.2 * 16 - 1),
) - camera)
.as_vec3();
let max = min + Vec3::splat(18.0);
let near = Vec3::ZERO.max(min).min(max).length_squared() <= 64.0 * 64.0;
near.then(|| block_entities.get(&key)).flatten()
})
.flatten()
}
/// The shadow camera's box that the shadow pass draws terrain in: the sides of `view.cull`, and
/// its near and far planes only with `depth_planes`.
fn shadow_box(view: &pack_post::ShadowView) -> Frustum {
let mut planes = frustum(view.cull);
if !view.depth_planes {
// `frustum`'s faces at depth 0 and 1 take in everything.
planes[4] = Vec4::W;
planes[5] = Vec4::W;
}
Frustum { planes }
}
/// Camera-relative clip planes (`Frustum`).
#[derive(Clone, Copy, Debug)]
pub struct Frustum {
@@ -13412,6 +13468,7 @@ mod shadow_face_tests {
unculled: None,
every_face,
view_casters: true,
depth_planes: false,
}
}
@@ -13431,6 +13488,24 @@ mod shadow_face_tests {
mod shadow_caster_tests {
use super::*;
#[test]
fn the_shadow_pass_takes_the_block_entities_of_its_sections_within_64_blocks() {
// Sections 64 to 79 high (the world from -64): a chest in each of three. The shadow
// pass drew `near` and `far`; `left_out` lies as near but the pass didn't draw it, and
// `far`'s nearest block is 78.5 blocks away.
let (near, left_out, far) = ((0, 8, 0), (1, 8, 0), (5, 8, 0));
let mut block_entities = FxHashMap::default();
block_entities.insert(near, vec![([3, 70, 3], 7)]);
block_entities.insert(left_out, vec![([20, 70, 3], 7)]);
block_entities.insert(far, vec![([85, 70, 3], 7)]);
let camera = DVec3::new(0.5, 70.0, 0.5);
let drawn = [near, far].into_iter();
let taken: Vec<_> = block_entities_within_reach(drawn, &block_entities, camera, -64)
.copied()
.collect();
assert_eq!(taken, vec![([3, 70, 3], 7)]);
}
/// What the shadow pass keeps of a camera-relative section at `min`, the camera at the
/// origin looking along -X (yaw 90) with the scenes' 70 degree, 16:9 view, and the light in
/// direction `light`; the light's own box takes in everything.
@@ -13585,6 +13660,52 @@ mod shadow_caster_tests {
);
}
/// Our probe pack's Iris 1.11.7 shadow maps, under a noon sun with `shadowDistance` 128, hold
/// the terrain at every depth along the light whatever planes the pack gives (#10403): from
/// a cave at y -7, the surface's sections ~70 blocks toward the light with `shadowNearPlane`
/// 0.05 and `shadowFarPlane` 256; from y 600, the ground ~535 blocks away with no planes set;
/// and both with the planes at 200 and 210. Only the box's sides cull.
#[test]
fn the_shadow_pass_draws_terrain_outside_the_pack_s_planes() {
let kept = |near: Option<f32>, far: Option<f32>, y: f64, min: [f32; 3], depth: bool| {
let shadow = horizon_shaders::Shadow {
distance: 128.0,
near,
far,
..horizon_shaders::Shadow::default()
};
let camera = DVec3::new(13.5, y + 1.62, -13.5);
let view = pack_post::ShadowView {
cull: pack_uniforms::shadow_culling_at(Vec3::Y, camera, &shadow),
light: Vec3::Y,
distance: None,
unculled: None,
every_face: true,
view_casters: false,
depth_planes: depth,
};
let cull = ShadowCull {
light: shadow_box(&view),
casters: None,
reach: None,
};
let min = Vec3::from(min);
cull.intersects(min, min + 16.0)
};
// The section holding the surface (y 64..80) above the cave, and the ground under y 600.
let above = [-8.0, 64.0 - (-7.0 + 1.62), -8.0];
let below = [-8.0, 64.0 - (600.0 + 1.62), -8.0];
assert!(kept(Some(0.05), Some(256.0), -7.0, above, false));
assert!(kept(None, None, 600.0, below, false));
assert!(kept(Some(200.0), Some(210.0), -7.0, above, false));
assert!(kept(Some(200.0), Some(210.0), 600.0, below, false));
// A section 200 blocks off to the side is still outside the box.
let side = [200.0, 0.0, -8.0];
assert!(!kept(Some(0.05), Some(256.0), -7.0, side, false));
// Horizon's own packs keep culling by their planes.
assert!(!kept(Some(0.05), Some(256.0), -7.0, above, true));
}
/// The margin doesn't follow the field of view: from the same camera under a noon sun, our
/// probe's Iris 1.11.7 maps keep a section that needs a growth of 1.1224 blocks at a field of
/// view of 70 (77° flying) and drop ones that need 1.1252 to 1.1422 at 50 (55° flying) and
@@ -368,6 +368,7 @@ impl NativeFrame {
unculled: None,
every_face: false,
view_casters: false,
depth_planes: true,
})
}
@@ -610,13 +610,17 @@ impl PackAtlas {
// A vertex's place in its section goes down to the 32768th of a block below it: read back
// with a probe pack of ours, Iris 1.11.7's terrain `gl_Vertex` has that for every position
// we read (water tops at 8/9 - 0.001, plants' offsets), on all three axes (#8464). A fit,
// not Iris's code. Whole and half blocks, and every 1/16, are on that grid already.
// not Iris's code. Whole and half blocks, and every 1/16, are on that grid already. A place
// up to 1/32 of a 32768th (one float step below 16) under a grid line is on it: plants whose
// random offset is 9/15 of a block read exactly on a half block in Iris, and a 32768th
// under it in Horizon (our guess, not tested: Horizon's float sum lands just under the
// line) (#8464). Also a fit.
let body = format!(
" vec4 hz_origin = {origin};\n \
uint hz_layer = hz_v_layer & 32767u;\n \
{cell}\
hz_scale = vec2({:?}, {:?}) * {side};\n \
hz_Vertex = vec4(floor(hz_v_pos * 32768.0) / 32768.0 + hz_origin.xyz, 1.0);\n \
hz_Vertex = vec4(floor(hz_v_pos * 32768.0 + 0.03125) / 32768.0 + hz_origin.xyz, 1.0);\n \
hz_uv_scale = hz_origin.w > 0.0 ? uintBitsToFloat(floatBitsToUint(hz_origin.w) & 0xff800000u) : fract(abs(hz_origin.w));\n \
hz_mid = vec2(0.5);\n \
hz_MultiTexCoord0 = vec4((hz_cell + clamp(vec2(hz_v_uv) * hz_uv_scale, 0.0, 1.0)) * hz_scale, 0.0, 1.0);\n \
@@ -174,7 +174,8 @@ fn flat_draws_map_uvs_onto_a_stitched_sprite() {
/// Read back with a probe pack of ours, Iris 1.11.7's terrain `gl_Vertex` (camera on the block
/// grid) puts a water top, 8/9 - 0.001 of a block up, at 29094/32768, and plants offset by 13/15
/// and 0.1 of a block at 28398/32768 and 3276/32768 (#8464): each 32768th below the position.
/// Whole blocks and sixteenths stay where they are.
/// Whole blocks and sixteenths stay where they are, and so does a place float rounding put a hair
/// below a grid line: Iris reads plants offset by 9/15 exactly on a half block.
#[test]
fn terrain_vertices_go_down_to_the_32768th_below_them() {
let instance = crate::gfx::test_gpu::instance();
@@ -204,6 +205,15 @@ fn terrain_vertices_go_down_to_the_32768th_below_them() {
([0.1, water, 13.0 / 15.0], [3276.0, 29094.0, 28398.0]),
([13.0 / 15.0, 0.1, water], [28398.0, 3276.0, 29094.0]),
([1.0, 0.5, 15.9375], [32768.0, 16384.0, 522240.0]),
// One float step below 1, 0.5 and 9.5.
(
[
1.0 - f32::EPSILON / 2.0,
0.5 - f32::EPSILON / 4.0,
9.5 - f32::EPSILON * 8.0,
],
[32768.0, 16384.0, 311296.0],
),
];
let vertices: Vec<_> = cases
.iter()
@@ -113,6 +113,8 @@ enum SamplerKind {
NearestRepeat,
/// The block atlas: nearest texels, blended between the two nearest levels.
Atlas,
/// colortex buffers: linear, blended between the two nearest levels.
Buffer,
Comparison,
}
@@ -140,15 +142,18 @@ impl SamplerKind {
(true, false) => Self::Repeat,
(false, false) => Self::NearestRepeat,
},
Source::Color(_) | Source::ShadowColor(_) | Source::Dummy | Source::Lightmap => {
Self::Linear
}
// Iris 1.11.7 blends a buffer's two nearest mipmap levels (our probe packs, #9307:
// every colortex read at levels 1.25 and 2.5 in composite and final).
Source::Color(_) => Self::Buffer,
Source::ShadowColor(_) | Source::Dummy | Source::Lightmap => Self::Linear,
}
}
fn binding_type(self) -> wgpu::SamplerBindingType {
match self {
Self::Linear | Self::Repeat | Self::Atlas => wgpu::SamplerBindingType::Filtering,
Self::Linear | Self::Repeat | Self::Atlas | Self::Buffer => {
wgpu::SamplerBindingType::Filtering
}
Self::Nearest | Self::NearestRepeat => wgpu::SamplerBindingType::NonFiltering,
Self::Comparison => wgpu::SamplerBindingType::Comparison,
}
@@ -660,6 +665,8 @@ pub struct PackPost {
repeat: wgpu::Sampler,
nearest_repeat: wgpu::Sampler,
atlas_sampler: wgpu::Sampler,
/// colortex buffers' (`SamplerKind::Buffer`).
buffer_sampler: wgpu::Sampler,
/// None when it would write nothing: colortex0 holds integers and there is no depth to copy
/// nor buffer to clear.
blit: Option<(wgpu::RenderPipeline, wgpu::BindGroupLayout)>,
@@ -1131,6 +1138,11 @@ pub(super) struct ShadowView {
/// maps fit keeping only those with `shadow.culling` unset (#8464): under a noon sun, only
/// the terrain above the ground the camera sees; with a low sun ahead, none behind the camera.
pub view_casters: bool,
/// Whether the projection's near and far planes in `cull` drop sections too. Our probes'
/// Iris 1.11.7 maps hold the terrain at every depth we put it along the light (from 535 blocks
/// beyond the shadow camera to about 120 before it), whatever `shadowNearPlane` and
/// `shadowFarPlane` the pack sets, or none (#10403): only its sides cull.
pub depth_planes: bool,
}
/// Set 3 of flat terrain, read by the vertex stage: the sprite layers' table (`mc_Entity`'s id
@@ -2978,6 +2990,13 @@ impl PackPost {
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
}),
buffer_sampler: device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Shader pack buffers"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
}),
blit,
copy,
upscale,
@@ -3189,6 +3208,7 @@ impl PackPost {
SamplerKind::Repeat => &self.repeat,
SamplerKind::NearestRepeat => &self.nearest_repeat,
SamplerKind::Atlas => &self.atlas_sampler,
SamplerKind::Buffer => &self.buffer_sampler,
SamplerKind::Comparison => shadow.map_or(&self.shadow.1, |t| &t.comparison),
};
entries.push(wgpu::BindGroupEntry {
@@ -4037,6 +4057,7 @@ impl PackPost {
},
every_face: true,
view_casters: true,
depth_planes: false,
})
}
@@ -4122,6 +4143,14 @@ impl PackPost {
}
self.setup_pending = true;
self.rgb_unset = true;
// Free the old targets before making the new ones: a pack with large buffers can't
// hold both sets at once on a card it otherwise fits (a window resize ran out of video
// memory). The bind groups hold the old views too, and the GPU may still be using
// them, so wait for it; a resize is rare.
if self.targets.take().is_some() {
self.forget_groups();
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
let sizes: [[u32; 2]; BUFFERS] =
std::array::from_fn(|i| self.sizes[i].map_or(size, |s| s.resolve(size)));
let full_chain = |size: [u32; 2]| 32 - size[0].max(size[1]).max(1).leading_zeros();
@@ -4580,10 +4609,8 @@ impl PackPost {
.get_or_insert_with(|| super::pack_center_depth::CenterDepth::new(device))
.collect(device);
self.uniforms.update(frame, inputs, size);
// Until the camera's biome is known, `smooth` waits for the world's values.
if !inputs.biome_known {
self.custom.restart_smoothing();
}
// `smooth` starts on the first frame that knows the camera's biome.
self.custom.world_frame(inputs.biome_known);
self.uniforms.eval_custom(&mut self.custom);
if self.targets.is_none() {
return;
@@ -5679,3 +5706,6 @@ mod unknown_sampler_terrain_tests;
#[cfg(test)]
mod mipmap_box_tests;
#[cfg(test)]
mod colortex_mip_blend_tests;
@@ -0,0 +1,61 @@
//! A buffer's mipmaps are read blended between the two nearest levels: our probe packs (#9307)
//! read every colortex at levels 2.5 and 1.25 in composite and final, in Iris 1.11.7, and got the
//! two nearest levels mixed.
use super::destination_tests::Fixture;
use super::*;
use std::io::{Cursor, Write};
const VERTEX: &str = "#version 120\nvarying vec2 uv;\nvoid main() { uv=gl_MultiTexCoord0.xy; gl_Position=ftransform(); }\n";
/// composite writes 1.0 into the left half of a 4x4 colortex1 and 0 into the right half, so at
/// (0.25, 0.5) level 1 reads 1.0 and level 2 (1x1) the mean, 0.5. final shows the read at
/// level 1.5 in red and at level 1.25 in green.
fn pack() -> Post {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let mut add = |name: &str, source: &str| {
zip.start_file(
format!("shaders/{name}"),
zip::write::SimpleFileOptions::default(),
)
.unwrap();
zip.write_all(source.as_bytes()).unwrap();
};
add("composite.vsh", VERTEX);
add(
"composite.fsh",
"#version 330 compatibility\n/*\nconst int colortex1Format = RGBA16F;\n*/\n\
/* RENDERTARGETS: 1 */\nlayout(location = 0) out vec4 o;\n\
void main() {\n o = vec4(gl_FragCoord.x < 2.0 ? 1.0 : 0.0, 0.0, 0.0, 1.0);\n}\n",
);
add("final.vsh", VERTEX);
add(
"final.fsh",
"#version 330 compatibility\n/*\nconst bool colortex1MipmapEnabled = true;\n*/\n\
uniform sampler2D colortex1;\n\
void main() {\n vec2 at = vec2(0.25, 0.5);\n\
gl_FragColor = vec4(textureLod(colortex1, at, 1.5).r, textureLod(colortex1, at, 1.25).r, 0.0, 1.0);\n}\n",
);
let pack = horizon_shaders::Pack::from_zip_bytes(
"original-colortex-mip-blend",
zip.finish().unwrap().into_inner(),
)
.unwrap();
let mut macros = horizon_shaders::Macros::standard();
let (properties, _) = horizon_shaders::read_properties(&pack, &mut macros);
let post = horizon_shaders::load_post(&pack, "", &properties, &macros, &Default::default());
assert_eq!(post.programs.len(), 2, "{:?}", post.notes);
post
}
#[test]
fn a_buffer_blends_its_two_nearest_levels() {
let Some(gpu) = Fixture::new() else { return };
let mut post = gpu.build(pack());
let pixels = gpu.render(&mut post, [4, 4]);
// Level 1.5: 1.0 and 0.5 mixed 1:1, 0.75; level 1.25: mixed 3:1, 0.875.
let [r, g, ..] = pixels[0];
assert!(
r.abs_diff(191) <= 2 && g.abs_diff(223) <= 2,
"levels 1.5 and 1.25: got red {r}, green {g}, expected 191 and 223"
);
}
@@ -669,6 +669,31 @@ fn shadow_depth_filtering(nearest: &str) -> Option<Vec<[u8; 4]>> {
/// `shadow_depth_filtering`'s shadow map read by `reads`: `final`'s code after its declarations
/// of shadowtex0 and shadowtex1.
fn shadow_depth_reads(nearest: &str, reads: &str) -> Option<Vec<[u8; 4]>> {
// The shadow map's depth runs from 0 at its left edge to 1 at its right.
let ramp = [
[-1., -1., 0.],
[1., -1., 0.],
[1., 1., 0.],
[-1., -1., 0.],
[1., 1., 0.],
[-1., 1., 0.],
];
shadow_depth_map_reads(
nearest,
reads,
"#version 120\nvoid main(){gl_Position=vec4(gl_Vertex.xy,gl_Vertex.x,1.0);}",
ramp,
)
}
/// A shadow map drawn by `shadow_vsh` from the two triangles `positions` (the rest keeps its
/// clear depth, the far plane), read by `reads` as in `shadow_depth_reads`.
fn shadow_depth_map_reads(
nearest: &str,
reads: &str,
shadow_vsh: &str,
positions: [[f32; 3]; 6],
) -> Option<Vec<[u8; 4]>> {
let gpu = Gpu::new()?;
let color = gpu.texture(T::Rgba8Unorm);
let depth = gpu.texture(T::Depth32Float);
@@ -704,11 +729,7 @@ fn shadow_depth_reads(nearest: &str, reads: &str) -> Option<Vec<[u8; 4]>> {
uniform sampler2D shadowtex0;\nuniform sampler2D shadowtex1;\n{reads}"
),
),
// The shadow map's depth runs from 0 at its left edge to 1 at its right.
(
"shadow.vsh".into(),
"#version 120\nvoid main(){gl_Position=vec4(gl_Vertex.xy,gl_Vertex.x,1.0);}".into(),
),
("shadow.vsh".into(), shadow_vsh.into()),
(
"shadow.fsh".into(),
format!("#version 120\n{settings}void main(){{gl_FragData[0]=vec4(1.0);}}"),
@@ -740,14 +761,6 @@ fn shadow_depth_reads(nearest: &str, reads: &str) -> Option<Vec<[u8; 4]>> {
inputs,
);
assert!(post.has_shadow(), "{:?}", post.notes);
let positions: [[f32; 3]; 6] = [
[-1., -1., 0.],
[1., -1., 0.],
[1., 1., 0.],
[-1., -1., 0.],
[1., 1., 0.],
[-1., 1., 0.],
];
let vertices = gpu
.device
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -793,6 +806,40 @@ fn shadow_depth_reads_filter_directly_and_with_a_texel_offset() {
}
}
#[test]
fn shadow_depth_filter_weights_are_256ths_as_in_our_iris_readings() {
// The map is at the far plane but for texel (8, 8), which is nearer. Reads between texels
// (7, 7) and (8, 8) give (8, 8)'s weight: Iris 1.11.7's filtered reads on our NVIDIA GPU
// fit weights of 1/256 along each axis, rounded half up, and a corner's product weight
// rounded half up to 1/256 (#8464). Red: a read a + 0.25/256 of the way along x only
// moves a/256 (77). Green: at (77.25, 77.25)/256 along both, (8, 8) weighs 23/256
// (77 × 77 / 256 = 23.16; exact weights would give 23.31).
let reads = "void main() {\n\
float far = texelFetch(shadowtex0, ivec2(7, 7), 0).r;\n\
float near = texelFetch(shadowtex0, ivec2(8, 8), 0).r;\n\
float f = 77.25 / 256.0;\n\
float along = texture(shadowtex0, vec2(7.5 + f, 8.5) / 16.0).r;\n\
float corner = texture(shadowtex0, vec2(7.5 + f, 7.5 + f) / 16.0).r;\n\
float x = (far - along) / (far - near) * 256.0;\n\
float w = (far - corner) / (far - near) * 256.0;\n\
gl_FragColor = vec4(abs(x - 77.0) < 0.01 ? 1.0 : 0.0,\n\
abs(w - 23.0) < 0.01 ? 1.0 : 0.0, 0.0, 1.0);\n\
}\n";
// Texel (8, 8) of the 16² map: x and y from 0 to 1/8 in clip space, depth 0.
let bump = [
[0., 0., -1.],
[0.125, 0., -1.],
[0.125, 0.125, -1.],
[0., 0., -1.],
[0.125, 0.125, -1.],
[0., 0.125, -1.],
];
let vsh = "#version 120\nvoid main(){gl_Position=vec4(gl_Vertex.xyz,1.0);}";
if let Some(pixels) = shadow_depth_map_reads("", reads, vsh, bump) {
all_pixels(&pixels, [255, 255, 0, 255], "shadowtex filter weights");
}
}
#[test]
fn shadowtex_nearest_settings_read_the_nearest_texel() {
for (line, expected) in [
@@ -552,7 +552,12 @@ fn invert_generic(m: Mat4) -> Mat4 {
/// `shadow_camera`'s view and projection in wgpu's clip space (depth 0..1), for culling.
pub fn shadow_culling(frame: &Frame, shadow: &Shadow) -> Mat4 {
let (view, gl) = shadow_camera(shadow_light(frame, shadow), frame.camera, shadow);
shadow_culling_at(shadow_light(frame, shadow), frame.camera, shadow)
}
/// `shadow_culling` for the direction toward the light and the camera's position.
pub fn shadow_culling_at(light: Vec3, camera: DVec3, shadow: &Shadow) -> Mat4 {
let (view, gl) = shadow_camera(light, camera, shadow);
// GL's depth -w..w to wgpu's 0..w.
let depth = Mat4::from_cols(
Vec4::X,
@@ -1840,9 +1840,10 @@ impl GraphicsUi {
self.scroll.release();
self.dragging.take().is_some()
}
/// The mouse wheel at the cursor. It scrolls the list, except that a hovered value
/// stepped by `<` `>` takes the step (`CycleButton.mouseScrolled`: up goes back)
/// while the "Scroll wheel changes settings" extra is on. A slider, a tick box and a
/// The mouse wheel at the cursor. It scrolls the list, as vanilla's does
/// (`AbstractScrollArea.mouseScrolled`), except that a hovered value stepped by
/// `<` `>` takes the step (up goes back) while the "Scroll wheel changes settings"
/// extra is on. A slider, a tick box and a
/// focused row don't take it, wherever the focus came from: the keyboard and a
/// controller step them with Left and Right.
pub fn wheel(
@@ -2526,6 +2527,25 @@ mod tests {
assert_eq!(ui.scroll.amount, 0.0);
}
#[test]
fn a_hovered_value_takes_the_wheel_only_with_the_extra_on() {
let mut s = Settings::default();
let mut ui = GraphicsUi::default();
let item = Item::Vanilla(Opt::AttackIndicator);
let r = row(&mut ui, &s, item);
let before = format!("{s:?}");
let actions = ui.wheel(&mut s, SIZE, (r[0] + 4.0, r[1] + 4.0), FB, -1.0);
assert_eq!(format!("{s:?}"), before, "the hovered value stays put");
assert!(actions.is_empty());
s.extras.wheel_cycles_settings = true;
let r = row(&mut ui, &s, item);
let before = format!("{s:?}");
let actions = ui.wheel(&mut s, SIZE, (r[0] + 4.0, r[1] + 4.0), FB, -1.0);
assert_ne!(format!("{s:?}"), before, "the hovered value steps");
assert!(!actions.is_empty());
}
#[test]
fn the_arrows_move_between_rows_in_view_and_over_to_the_page_list() {
let mut s = Settings::default();
+52 -7
View File
@@ -1232,7 +1232,8 @@ impl HudAtlas {
.ok();
let animation = animation_of(mcmeta.as_deref());
// An animated strip keeps its first frame.
let frames = name.starts_with("block/") || name.starts_with("item/");
let frames =
animation.is_some() || name.starts_with("block/") || name.starts_with("item/");
let (w, h, pixels) = if frames {
first_frame(image, animation.as_ref())
} else {
@@ -1313,12 +1314,13 @@ impl HudAtlas {
edition.3 = pixels;
}
images.push(("horizon/white".into(), 4, 4, vec![255; 64]));
images.push((
crate::pad_cursor::SPRITE.into(),
crate::pad_cursor::SIZE,
crate::pad_cursor::SIZE,
crate::pad_cursor::pixels(),
));
// The controller's art, unless a pack brought its own (walked above, as a mod's GUI
// textures are).
for image in crate::pad_glyph::images() {
if !images.iter().any(|i| i.0 == image.0) {
images.push(image);
}
}
if full {
Self::slots(&mut images, crate::trims::slot_size(root), mod_image);
// MissingTextureAtlasSprite.generateMissingImage: 16x16, magenta
@@ -3495,6 +3497,49 @@ mod tests {
let banner = atlas.sprites["custom/banner"];
assert_eq!(banner.texels((atlas.width, atlas.height)), (16, 32));
}
#[test]
fn an_animated_gui_sprite_packs_its_first_frame_and_animates() {
let root =
std::env::temp_dir().join(format!("horizon-atlas-gui-anim-{}", std::process::id()));
let sprites = root.join("assets/minecraft/textures/gui/sprites/icon");
fs::create_dir_all(&sprites).unwrap();
// A GUI sprite like icon/music_notes: 4 frames stacked vertically (16x64),
// with animation metadata, and a still 16x32 texture beside it.
write_strip(
&sprites.join("music_notes.png"),
16,
64,
&[
[10, 20, 30, 255],
[40, 50, 60, 255],
[70, 80, 90, 255],
[100, 110, 120, 255],
],
Some(r#"{"animation": {"frametime": 2}}"#),
);
write_strip(
&sprites.join("static_badge.png"),
16,
32,
&[[9, 9, 9, 255]],
None,
);
let atlas = HudAtlas::build(&root).unwrap();
let _ = fs::remove_dir_all(&root);
let rect = atlas.sprites["gui/sprites/icon/music_notes"];
// One frame, not the whole 16x64 strip squeezed into a slot.
assert_eq!(rect.texels((atlas.width, atlas.height)), (16, 16));
assert_eq!(texel_at(&atlas, rect, 0, 15), [10, 20, 30, 255]);
let (_, animation) = atlas
.sprite_animations
.iter()
.find(|(r, _)| r.u0 == rect.u0 && r.v0 == rect.v0)
.expect("the gui sprite animates");
assert_eq!(animation.frame_count(), 4);
// A still texture there stays whole.
let static_badge = atlas.sprites["gui/sprites/icon/static_badge"];
assert_eq!(static_badge.texels((atlas.width, atlas.height)), (16, 32));
}
/// A resource root with one plain item texture and these CIT files (relative path, contents).
fn cit_root(name: &str, files: &[(&str, &str)]) -> PathBuf {
let root =
+2
View File
@@ -63,6 +63,7 @@ pub mod menu_blur;
pub mod mesher;
pub mod minimap;
pub mod mipmap;
pub mod mod_browser;
pub mod mod_list;
pub mod model_library;
pub mod models;
@@ -76,6 +77,7 @@ pub mod pack_browser;
pub mod pack_screen;
pub mod packs;
pub mod pad_cursor;
pub mod pad_glyph;
pub mod par;
pub mod particles;
pub mod poi_sections;
@@ -0,0 +1,189 @@
//! What Options > Mods > Browse mods shows (docs/design/mod-hotload.md): the
//! registry's content mods this game installs, a search over them, and the one
//! whose page is open. The game fills it from a worker
//! (`horizon_client::mod_registry`, `game/mod_browser.rs`); the options screen
//! only reads it and asks for installs through its events.
/// One project of the registry, as the list and its page show it.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Listing {
pub id: String,
pub name: String,
pub summary: String,
/// Markdown, shown as plain text.
pub description: String,
pub authors: Vec<String>,
pub license: String,
pub categories: Vec<String>,
pub tags: Vec<String>,
/// The version this game would install, its date and changelog.
pub version: String,
pub published: String,
pub changelog: String,
pub downloads: u64,
/// The project's page on the website.
pub page: String,
/// The version installed in this game folder, if any.
pub installed: Option<String>,
}
impl Listing {
/// Whether a search for `query` (already lower case) finds it: its name,
/// id, summary, authors, categories or tags.
pub fn matches(&self, query: &str) -> bool {
let query = query.trim();
query.is_empty()
|| [&self.name, &self.id, &self.summary]
.into_iter()
.chain(&self.authors)
.chain(&self.categories)
.chain(&self.tags)
.any(|field| field.to_lowercase().contains(query))
}
/// Installed, but not the version the registry offers now.
pub fn has_update(&self) -> bool {
self.installed
.as_ref()
.is_some_and(|installed| *installed != self.version)
}
}
/// Where the browser is.
#[derive(Clone, Debug, Default, PartialEq)]
pub enum Status {
/// Not asked yet.
#[default]
Idle,
/// The index is being read.
Loading,
/// The projects are listed.
Ready,
/// The index couldn't be read: why, in English (shown inside a translated line).
Failed(String),
}
#[derive(Clone, Debug, Default)]
pub struct ModBrowser {
pub status: Status,
pub listings: Vec<Listing>,
/// The project an install runs for.
pub installing: Option<String>,
/// The last install's outcome, already translated, for the page it ran on.
pub message: Option<(String, bool)>,
}
impl ModBrowser {
/// The listings a search for `query` finds, by index into `listings`.
pub fn found(&self, query: &str) -> Vec<usize> {
let query = query.to_lowercase();
(0..self.listings.len())
.filter(|&i| self.listings[i].matches(&query))
.collect()
}
pub fn listing(&self, id: &str) -> Option<&Listing> {
self.listings.iter().find(|l| l.id == id)
}
}
/// A project's markdown description as lines of plain text for the page:
/// heading marks, emphasis and list bullets dropped, links shown as their text,
/// blank lines and fences left out.
pub fn plain_lines(markdown: &str) -> Vec<String> {
let mut out = Vec::new();
for line in markdown.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with("```") || line.starts_with("<!--") {
continue;
}
let line = line.trim_start_matches('#').trim_start();
let line = match line.strip_prefix("- ").or_else(|| line.strip_prefix("* ")) {
Some(rest) => format!("• {rest}"),
None => line.to_owned(),
};
let line = links_as_text(&line).replace("**", "").replace('`', "");
if !line.is_empty() {
out.push(line);
}
}
out
}
/// `[text](url)` → `text`, `![alt](url)` → nothing.
fn links_as_text(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut rest = line;
while let Some(open) = rest.find('[') {
let Some(close) = rest[open..].find("](").map(|c| open + c) else {
break;
};
let Some(end) = rest[close..].find(')').map(|e| close + e) else {
break;
};
let image = open > 0 && rest[..open].ends_with('!');
out.push_str(&rest[..if image { open - 1 } else { open }]);
if !image {
out.push_str(&rest[open + 1..close]);
}
rest = &rest[end + 1..];
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn listing(id: &str, tags: &[&str]) -> Listing {
Listing {
id: id.into(),
name: id.to_uppercase(),
summary: format!("All about {id}"),
tags: tags.iter().map(|t| (*t).to_owned()).collect(),
version: "1.0".into(),
..Listing::default()
}
}
#[test]
fn a_search_finds_names_tags_and_categories_in_any_case() {
let mut pipes = listing("pipes", &["tech"]);
pipes.categories = vec!["technology".into()];
let browser = ModBrowser {
listings: vec![listing("ruby", &["gems"]), pipes],
..ModBrowser::default()
};
assert_eq!(browser.found(""), [0, 1]);
assert_eq!(browser.found("RUBY"), [0]);
assert_eq!(browser.found("gems"), [0]);
assert_eq!(browser.found("techno"), [1]);
assert!(browser.found("nothing like it").is_empty());
}
#[test]
fn an_update_is_another_version_than_the_installed_one() {
let mut l = listing("ruby", &[]);
assert!(!l.has_update());
l.installed = Some("1.0".into());
assert!(!l.has_update());
l.installed = Some("0.9".into());
assert!(l.has_update());
}
#[test]
fn markdown_reads_as_plain_lines() {
let text = "# Ruby\n\nAdds **rubies**. See [the wiki](https://x.y).\n\n![shot](https://x.y/a.png)\n- one\n* two\n```\ncode\n```\n";
assert_eq!(
plain_lines(text),
[
"Ruby",
"Adds rubies. See the wiki.",
"• one",
"• two",
"code"
]
);
}
}
+3 -3
View File
@@ -93,7 +93,7 @@ pub(crate) fn prepare_with_models(
models: &crate::model_library::ParsedModels,
) -> Result<PreparedModels> {
let facts: std::collections::BTreeMap<String, Value> =
serde_json::from_slice(&std::fs::read(root.join("model-states.json"))?)?;
horizon_world::json_file::read(&root.join("model-states.json"))?;
let blocks: Vec<&str> = facts
.keys()
.filter(|name| registry.default_state(name).is_some())
@@ -158,11 +158,11 @@ pub fn load_visibility(
collision::CachedCollisionRegistry,
registry::BlockTableFile,
};
use std::{collections::BTreeMap, fs};
use std::collections::BTreeMap;
let file = BlockTableFile::open(&root.join("occlusion.nbt"), registry)?;
let occlusion = CachedCollisionRegistry::from_table(file.table("occlusion")?)?;
let raw: BTreeMap<String, Value> =
serde_json::from_slice(&fs::read(root.join("render-definitions.json"))?)?;
horizon_world::json_file::read(&root.join("render-definitions.json"))?;
let definitions = raw
.into_iter()
.map(|(name, v)| {
+400 -115
View File
@@ -9,7 +9,7 @@ pub use crate::settings::Opt;
use crate::{
gamepad::{self, GlyphStyle},
hud::{HudAtlas, HudBatch, rgba},
keybinds::{self, CATEGORIES, KeyBinds, MAPPINGS},
keybinds::{self, CATEGORIES, KeyBinds, all_mappings},
lang::{self, LanguageInfo},
settings::{SOUND_SOURCES, Settings},
text::{self, Style, Text},
@@ -31,6 +31,30 @@ fn percent(caption: &str, value: f32) -> Text {
],
)
}
/// `text` cut into lines of at most `width` characters, at spaces where it has them.
fn wrap(text: &str, width: usize) -> Vec<String> {
let mut lines = Vec::new();
let mut line = String::new();
for word in text.split_whitespace() {
let len = line.chars().count();
if len > 0 && len + 1 + word.chars().count() > width {
lines.push(std::mem::take(&mut line));
}
if !line.is_empty() {
line.push(' ');
}
line.push_str(word);
while line.chars().count() > width {
let rest: String = line.chars().skip(width).collect();
line = line.chars().take(width).collect();
lines.push(std::mem::replace(&mut line, rest));
}
}
if !line.is_empty() {
lines.push(line);
}
lines
}
fn on_off(v: bool) -> Text {
tr(if v { "options.on" } else { "options.off" })
}
@@ -281,6 +305,7 @@ impl Opt {
Opt::PadToggleSneak => "horizon.options.sneak_button",
Opt::PadMoveDeadzone => "horizon.options.move_deadzone",
Opt::PadLookDeadzone => "horizon.options.look_deadzone",
Opt::PadTriggerThreshold => "horizon.options.trigger_threshold",
Opt::PadGlyphs => "horizon.options.button_glyphs",
Opt::PadKeyboard => "horizon.options.on_screen_keyboard",
Opt::ModelPart(i) => [
@@ -361,6 +386,7 @@ impl Opt {
Opt::PadRepeatInterval => unit(s.controller.repeat_interval, 0.03, 0.3),
Opt::PadMoveDeadzone => unit(s.controller.move_deadzone, 0.0, 0.5),
Opt::PadLookDeadzone => unit(s.controller.look_deadzone, 0.0, 0.5),
Opt::PadTriggerThreshold => unit(s.controller.trigger_threshold, 0.1, 0.9),
Opt::SprintWindow => unit(s.sprint_window as f32, 0.0, 10.0),
_ => Kind::Cycle,
}
@@ -425,6 +451,7 @@ impl Opt {
Opt::MenuBlur => Some(10),
Opt::NotificationDisplayTime => Some(95),
Opt::PadMoveDeadzone | Opt::PadLookDeadzone => Some(50),
Opt::PadTriggerThreshold => Some(80),
Opt::SprintWindow => Some(10),
_ => None,
}
@@ -634,11 +661,11 @@ impl Opt {
)],
),
),
Opt::PadMoveDeadzone | Opt::PadLookDeadzone => {
let v = if self == Opt::PadMoveDeadzone {
s.controller.move_deadzone
} else {
s.controller.look_deadzone
Opt::PadMoveDeadzone | Opt::PadLookDeadzone | Opt::PadTriggerThreshold => {
let v = match self {
Opt::PadMoveDeadzone => s.controller.move_deadzone,
Opt::PadLookDeadzone => s.controller.look_deadzone,
_ => s.controller.trigger_threshold,
};
Text::translate(
"options.percent_value",
@@ -897,6 +924,7 @@ impl Opt {
Opt::PadRepeatInterval => s.controller.repeat_interval = step(30.0, 300.0) / 1000.0,
Opt::PadMoveDeadzone => s.controller.move_deadzone = step(0.0, 50.0) / 100.0,
Opt::PadLookDeadzone => s.controller.look_deadzone = step(0.0, 50.0) / 100.0,
Opt::PadTriggerThreshold => s.controller.trigger_threshold = step(10.0, 90.0) / 100.0,
Opt::SprintWindow => s.sprint_window = step(0.0, 10.0) as i32,
_ => {}
}
@@ -1254,6 +1282,10 @@ pub enum Page {
Font,
Packs,
Mods,
/// Browse mods: the registry's content mods, searched (`mod_browser`).
ModBrowser,
/// One registry project's page: `OptionsUi::mod_project`.
ModProject,
/// A native mod's settings page: its index in `mods.installed`.
NativeMod(usize),
/// Horizon's shader packs, either kind (`shader_pack_list`).
@@ -1309,6 +1341,8 @@ enum Act {
ToggleMod(usize),
/// PvP & HUD's PvP preset (`pvp::apply_preset`).
PvpPreset,
/// PvP & HUD's Edit HUD button.
HudEditor,
/// Pick `s.shader_packs[i]`, or turn it off when it is the pick.
PickShaderPack(usize),
OpenShaderPackFolder,
@@ -1332,6 +1366,12 @@ enum Act {
NativeKey(usize, usize),
NativeButton(usize, usize),
OpenModFolder,
/// Browse mods: the page of listing `.0` of `mod_browser`.
ModProject(usize),
/// The open project page's Install or Update.
InstallMod,
/// The open project page's page on the website.
OpenModPage,
/// Accessibility's "Accessibility Help" link
/// (`AccessibilityOptionsScreen.addFooter`'s openLinkButton).
AccessibilityHelp,
@@ -1468,6 +1508,12 @@ pub enum Event {
/// Data Packs' Open Pack Folder: create the folder if needed and open it.
OpenDataPackFolder(std::path::PathBuf),
OpenModFolder,
/// Browse mods opened: read the registry's index into `mod_browser`.
BrowseMods,
/// Install (or update) the registry's content mod with this id.
InstallMod(String),
/// Open a registry project's page on the website.
OpenModPage(String),
/// Accessibility's Help link: confirm and open
/// `CommonLinks.ACCESSIBILITY_HELP` over the options screen.
AccessibilityHelp,
@@ -1491,6 +1537,8 @@ pub enum Event {
NativeButton(usize, usize),
/// Roll the credits (`CreditsAndAttributionScreen.openCreditsScreen`).
Credits,
/// PvP & HUD's Edit HUD: open the HUD editor over the game.
HudEditor,
/// Copy how an agent connects (the AI Agent page).
CopyAgentDetails,
/// The AI Agent page's recording button: start or stop.
@@ -1564,7 +1612,7 @@ pub struct OptionsUi {
pub selected_key: Option<&'static str>,
/// The native mod's key waiting: (mod, key) in `mods.installed`.
selected_native_key: Option<(usize, usize)>,
/// Controller Binds: the action waiting for a pad button.
/// Bindings: the action waiting for a pad button.
pub selected_pad_action: Option<&'static str>,
/// The glyphs the binds list shows (the setting, else the pad's family).
pub pad_style: GlyphStyle,
@@ -1578,6 +1626,15 @@ pub struct OptionsUi {
language_choice: Option<String>,
language_search: ui::EditBox,
pub(crate) settings_search: ui::EditBox,
/// Browse mods: the registry's projects, filled by the game.
pub mod_browser: crate::mod_browser::ModBrowser,
/// The project whose page is open (`Page::ModProject`).
pub mod_project: String,
/// The Mods page's line about switches that wait for a restart.
pub mods_note: Option<String>,
/// The content mods that switch with a world open: only assets and data,
/// in a world this game serves (docs/design/mod-hotload.md).
pub mods_live: std::collections::BTreeSet<String>,
/// Horizon's `packBrowser` extra: the pack screen's Add button.
pub pack_browser: bool,
/// Packs: the packs found and the selection the screen opens with.
@@ -1666,6 +1723,10 @@ impl Default for OptionsUi {
language_choice: None,
language_search: search,
settings_search,
mod_browser: Default::default(),
mod_project: String::new(),
mods_note: None,
mods_live: Default::default(),
pack_browser: false,
pack_infos: Vec::new(),
pack_selection: Vec::new(),
@@ -1993,6 +2054,7 @@ fn tooltip_text(s: &Settings, wd: &Widget) -> Option<Text> {
Act::Push(Page::Language) => Some("options.language.tooltip".into()),
Act::Push(Page::Accessibility) => Some("options.accessibility.tooltip".into()),
Act::PvpPreset => Some("horizon.pvp.preset.tooltip".into()),
Act::HudEditor => Some("horizon.pvp.editor.tooltip".into()),
_ => None,
};
if let Some(key) = key.filter(|key| lang::has(key)) {
@@ -2156,7 +2218,10 @@ impl OptionsUi {
}
fn settings_search_active(&self) -> bool {
self.settings_search.focused
&& matches!(self.page(), Page::Video | Page::Controls | Page::KeyBinds)
&& matches!(
self.page(),
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
)
}
/// Whether the resource pack screen is open (not Create World's data packs on the same
/// page).
@@ -2297,7 +2362,10 @@ impl OptionsUi {
self.focus = None;
self.focus_from_keyboard = false;
self.can_change_value = false;
if matches!(page, Page::Video | Page::Controls | Page::KeyBinds) {
if matches!(
page,
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
) {
self.settings_search.set_value("");
self.settings_search.focused = false;
}
@@ -2423,6 +2491,8 @@ impl OptionsUi {
Page::Font => "options.font.title",
Page::Packs => "resourcePack.title",
Page::Mods => "horizon.pages.mods",
Page::ModBrowser => "horizon.mods.browse.title",
Page::ModProject => "horizon.mods.browse.project",
Page::NativeMod(_) => "Mod Settings",
Page::ShaderPacks => "horizon.pages.shader_packs",
Page::ShaderOptions | Page::NativePackOptions(..) => "horizon.pages.shader_options",
@@ -2489,6 +2559,156 @@ impl OptionsUi {
filtered
}
/// Browse mods: what the registry offers this game, by the search box.
fn mod_browser_rows(&self) -> Vec<Row> {
use crate::mod_browser::Status;
let b = &self.mod_browser;
match &b.status {
Status::Idle | Status::Loading => {
return vec![Row::Note(lang::get("horizon.mods.browse.loading"))];
}
Status::Failed(why) => {
return vec![Row::Note(lang::tr("horizon.mods.browse.failed", &[why]))];
}
Status::Ready => {}
}
let found = b.found(&self.settings_search.value);
if found.is_empty() {
return vec![Row::Note(lang::get("horizon.mods.browse.none"))];
}
let mut rows = Vec::new();
for i in found {
let l = &b.listings[i];
let mut label = Text::literal(format!("{} {}", l.name, l.version));
if l.has_update() {
label = label.append(tr("horizon.mods.browse.has_update"));
} else if l.installed.is_some() {
label = label.append(tr("horizon.mods.browse.is_installed"));
}
rows.push(Row::Buttons(
button(0.0, 0.0, 310.0, label, Act::ModProject(i)),
None,
));
rows.extend(wrap(&l.summary, 64).into_iter().take(1).map(Row::Note));
}
rows
}
/// A registry project's page: what it is, Install or Update, its switch
/// once installed, its description and changelog.
/// A content mod's set feeds the block-state tables the open world's
/// chunks were decoded with: changing it mid-world remaps their ids
/// (Forge/Fabric freeze the registry before a world opens). One with only
/// assets and data reloads live, and a native mod toggles live through
/// `native_on`, so they stay available.
fn mod_frozen(&self, installed: &crate::mod_list::Installed) -> bool {
self.in_world && installed.native.is_none() && !self.mods_live.contains(&installed.id)
}
fn mod_project_rows(&self, s: &Settings) -> Vec<Row> {
let b = &self.mod_browser;
let Some(l) = b.listing(&self.mod_project) else {
return vec![Row::Note(lang::get("horizon.mods.browse.none"))];
};
let mut rows = vec![Row::Note(format!("{} {}", l.name, l.version))];
let by = [
lang::tr("horizon.mods.browse.by", &[&l.authors.join(", ")]),
l.license.clone(),
l.published.clone(),
];
let by: Vec<&str> = by
.iter()
.map(String::as_str)
.filter(|t| !t.is_empty())
.collect();
rows.push(Row::Note(by.join(" · ")));
let labels: Vec<&str> = l
.categories
.iter()
.chain(&l.tags)
.map(String::as_str)
.collect();
if !labels.is_empty() {
rows.push(Row::Note(labels.join(", ")));
}
let installing = b.installing.as_deref() == Some(l.id.as_str());
let install = match (&l.installed, installing) {
(_, true) => inactive(button(
0.0,
0.0,
150.0,
tr("horizon.mods.browse.installing"),
Act::None,
)),
(None, false) => button(
0.0,
0.0,
150.0,
tr("horizon.mods.browse.install"),
Act::InstallMod,
),
(Some(_), false) if l.has_update() => button(
0.0,
0.0,
150.0,
tr("horizon.mods.browse.update"),
Act::InstallMod,
),
(Some(_), false) => inactive(button(
0.0,
0.0,
150.0,
tr("horizon.mods.browse.installed"),
Act::None,
)),
};
let page = button(
0.0,
0.0,
150.0,
tr("horizon.mods.browse.open_page"),
Act::OpenModPage,
);
let row = s.mods.installed.iter().position(|m| m.id == l.id && !m.dev);
match row.filter(|_| l.installed.is_some()) {
Some(i) => {
let label =
Text::literal(format!("{}: ", lang::get("horizon.mods.browse.enabled")))
.append(on_off(s.mods.is_on(i)));
let toggle = button(0.0, 0.0, 150.0, label, Act::ToggleMod(i));
let toggle = match self.mod_frozen(&s.mods.installed[i]) {
true => inactive(toggle),
false => toggle,
};
rows.push(Row::Buttons(install, Some(toggle)));
rows.push(Row::Buttons(page, None));
}
None => rows.push(Row::Buttons(install, Some(page))),
}
if let Some((message, _)) = &b.message {
rows.extend(wrap(message, 64).into_iter().map(Row::Note));
}
let description = crate::mod_browser::plain_lines(if l.description.is_empty() {
&l.summary
} else {
&l.description
});
rows.extend(
description
.iter()
.flat_map(|line| wrap(line, 64))
.map(Row::Note),
);
let changelog = crate::mod_browser::plain_lines(&l.changelog);
if !changelog.is_empty() {
rows.push(Row::Heading("horizon.mods.browse.changelog".into()));
rows.extend(
changelog
.iter()
.flat_map(|line| wrap(line, 64))
.map(Row::Note),
);
}
rows
}
/// A native mod's page: its name, its keys as Key Binds rows (the
/// player's choice, else the default), then its buttons.
fn native_rows(&self, s: &Settings, i: usize) -> Vec<Row> {
@@ -2562,7 +2782,10 @@ impl OptionsUi {
let preset = button(0.0, 0.0, 150.0, tr("horizon.pvp.preset"), Act::PvpPreset);
let mut rows = Vec::new();
if self.page() == Page::Pvp {
rows.push(Row::Buttons(preset, None));
// The editor moves what the game shows: only from a world.
let edit = button(0.0, 0.0, 150.0, tr("horizon.pvp.editor"), Act::HudEditor);
let edit = if self.in_world { edit } else { inactive(edit) };
rows.push(Row::Buttons(preset, Some(edit)));
}
let mut section = Vec::new();
let flush = |rows: &mut Vec<Row>, section: &mut Vec<crate::extras::Item>| {
@@ -2857,61 +3080,62 @@ impl OptionsUi {
}
Page::Font => pairs(&[Opt::ForceUnicode, Opt::JapaneseGlyphs]),
Page::Mods => {
if s.mods.installed.is_empty() {
return vec![Row::Header("horizon.mods.empty")];
let browse = button(
0.0,
0.0,
310.0,
tr("horizon.mods.browse"),
Act::Push(Page::ModBrowser),
);
let mut rows = vec![Row::Buttons(browse, None)];
if let Some(note) = &self.mods_note {
rows.extend(wrap(note, 64).into_iter().map(Row::Note));
}
s.mods
.installed
.iter()
.enumerate()
.map(|(i, installed)| {
let mut label = Text::option_name_value(
Text::literal(installed.title.clone()),
on_off(s.mods.is_on(i)),
if s.mods.installed.is_empty() {
rows.push(Row::Header("horizon.mods.empty"));
return rows;
}
rows.extend(s.mods.installed.iter().enumerate().map(|(i, installed)| {
let mut label = Text::option_name_value(
Text::literal(installed.title.clone()),
on_off(s.mods.is_on(i)),
);
if installed.problem.as_deref() == Some(crate::mod_list::NOT_BUILT) {
label = label.append(tr("horizon.mods.not_built"));
} else if installed.problem.is_some() {
label = label.append(tr("horizon.mods.stopped"));
} else if installed.dev {
label = label.append(tr("horizon.mods.dev"));
}
let frozen = self.mod_frozen(installed);
if frozen {
label = label.append(tr("horizon.mods.in_world"));
}
let dead = installed.dev || installed.id.is_empty() || frozen;
// Only native mods with actual settings need a second button.
if installed
.native
.as_ref()
.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty())
{
let toggle = button(0.0, 0.0, 150.0, label, Act::ToggleMod(i));
let settings = button(
0.0,
0.0,
150.0,
tr("horizon.mods.settings"),
Act::ModSettings(i),
);
if installed.problem.as_deref() == Some(crate::mod_list::NOT_BUILT) {
label = label.append(tr("horizon.mods.not_built"));
} else if installed.problem.is_some() {
label = label.append(tr("horizon.mods.stopped"));
} else if installed.dev {
label = label.append(tr("horizon.mods.dev"));
}
// A content mod's set feeds the block-state tables the
// open world's chunks were decoded with: changing it
// mid-world remaps their ids (Forge/Fabric freeze the
// registry before a world opens). A native mod toggles
// live through `native_on`, so it stays available.
let content = installed.native.is_none();
if self.in_world && content {
label = label.append(tr("horizon.mods.in_world"));
}
let dead =
installed.dev || installed.id.is_empty() || self.in_world && content;
// Only native mods with actual settings need a second button.
if installed
.native
.as_ref()
.is_some_and(|page| !page.keys.is_empty() || !page.buttons.is_empty())
{
let toggle = button(0.0, 0.0, 150.0, label, Act::ToggleMod(i));
let settings = button(
0.0,
0.0,
150.0,
tr("horizon.mods.settings"),
Act::ModSettings(i),
);
Row::Buttons(
if dead { inactive(toggle) } else { toggle },
Some(settings),
)
} else {
let widget = button(0.0, 0.0, 310.0, label, Act::ToggleMod(i));
Row::Buttons(if dead { inactive(widget) } else { widget }, None)
}
})
.collect()
Row::Buttons(if dead { inactive(toggle) } else { toggle }, Some(settings))
} else {
let widget = button(0.0, 0.0, 310.0, label, Act::ToggleMod(i));
Row::Buttons(if dead { inactive(widget) } else { widget }, None)
}
}));
rows
}
Page::ModBrowser => self.mod_browser_rows(),
Page::ModProject => self.mod_project_rows(s),
Page::NativeMod(i) => self.native_rows(s, i),
Page::ShaderPacks => {
if s.shader_packs.is_empty() {
@@ -2989,7 +3213,10 @@ impl OptionsUi {
let mut rows = ListRows::default();
let page = self.page();
let cx = (w / 2.0).floor();
if matches!(page, Page::Video | Page::Controls | Page::KeyBinds) {
if matches!(
page,
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
) {
self.settings_search.x = cx - 100.0;
self.settings_search.y = 17.0;
}
@@ -3112,7 +3339,7 @@ impl OptionsUi {
row: 0,
});
let mut mappings: Vec<(&str, &str)> =
MAPPINGS.iter().map(|(n, _, c)| (*n, *c)).collect();
all_mappings().map(|(n, _, c)| (*n, *c)).collect();
let query = self.settings_search.value.to_lowercase();
mappings.retain(|(name, _)| {
query.is_empty() || lang::get(name).to_lowercase().contains(&query)
@@ -3206,7 +3433,7 @@ impl OptionsUi {
}
let reset_x = scrollbar_x - 50.0 - 10.0;
let change_x = reset_x - 5.0 - 75.0;
let name_static = MAPPINGS.iter().find(|m| m.0 == name).map_or("", |m| m.0);
let name_static = all_mappings().find(|m| m.0 == name).map_or("", |m| m.0);
out.push(button(change_x, y, 75.0, message, Act::Key(name_static)));
if !conflicts.is_empty() {
out.push(Widget {
@@ -3331,12 +3558,12 @@ impl OptionsUi {
// Drawn by `pack_screen`.
Page::Packs => {}
_ => {
if matches!(page, Page::Video | Page::Controls) {
if matches!(page, Page::Video | Page::Controls | Page::ModBrowser) {
top = 38.0;
}
out.push(Widget {
x: 0.0,
y: if matches!(page, Page::Video | Page::Controls) {
y: if matches!(page, Page::Video | Page::Controls | Page::ModBrowser) {
4.0
} else {
12.0
@@ -3915,23 +4142,30 @@ impl OptionsUi {
}
self.language_search = search;
}
if matches!(self.page(), Page::Video | Page::Controls | Page::KeyBinds) {
if matches!(
self.page(),
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
) {
let mut search = self.settings_search.clone();
if self.focus == Some(FocusId::Search) {
search.focused = true;
}
search.draw(batch, atlas, None);
let hint = match self.page() {
Page::ModBrowser => "horizon.mods.browse.search",
_ => "horizon.options.search",
};
// Horizon's search box sits in the header, before the list.
crate::widgets::last_narratable(|n| {
n.order = -1;
if let crate::widgets::What::EditBox { message, .. } = &mut n.what {
*message = tr("horizon.options.search").plain();
*message = tr(hint).plain();
}
});
if search.value.is_empty() && !search.focused {
batch.text_styled(
atlas,
&tr("horizon.options.search").with_color(text::GRAY).italic(),
&tr(hint).with_color(text::GRAY).italic(),
search.text_x(),
search.text_y(),
u32::MAX,
@@ -4091,7 +4325,12 @@ impl OptionsUi {
self.size = size;
self.framebuffer = framebuffer;
let mut events = Vec::new();
if !drag && matches!(self.page(), Page::Video | Page::Controls | Page::KeyBinds) {
if !drag
&& matches!(
self.page(),
Page::Video | Page::Controls | Page::KeyBinds | Page::ModBrowser
)
{
let search = &mut self.settings_search;
search.focused = ui::hovered((x, y), search.x, search.y, search.width, search.height);
if search.focused {
@@ -4247,6 +4486,9 @@ impl OptionsUi {
if p == Page::Music {
events.push(Event::ListMusicTracks);
}
if p == Page::ModBrowser {
events.push(Event::BrowseMods);
}
self.push(p)
}
Act::Done => events.extend(self.back(s)),
@@ -4297,10 +4539,26 @@ impl OptionsUi {
}
Act::NativeButton(i, b) => events.push(Event::NativeButton(i, b)),
Act::OpenModFolder => events.push(Event::OpenModFolder),
Act::ModProject(i) => {
if let Some(listing) = self.mod_browser.listings.get(i) {
self.mod_project = listing.id.clone();
self.push(Page::ModProject);
}
}
Act::InstallMod => {
self.mod_browser.message = None;
events.push(Event::InstallMod(self.mod_project.clone()));
}
Act::OpenModPage => {
if let Some(l) = self.mod_browser.listing(&self.mod_project) {
events.push(Event::OpenModPage(l.page.clone()));
}
}
Act::PvpPreset => {
crate::pvp::apply_preset(s);
events.push(Event::Changed);
}
Act::HudEditor => events.push(Event::HudEditor),
Act::AccessibilityHelp => events.push(Event::AccessibilityHelp),
Act::PickShaderPack(i) => {
if let Some(pack) = s.shader_packs.get(i) {
@@ -4489,19 +4747,19 @@ impl OptionsUi {
}
events
}
/// The mouse wheel at the cursor. Vanilla scrolls the list
/// (`AbstractScrollArea.mouseScrolled`), except that a hovered cycle button
/// takes the step and keeps the wheel (`CycleButton.mouseScrolled`: up goes
/// back); `AbstractSliderButton` does not override it, so a slider under the
/// cursor lets the list scroll.
/// The mouse wheel at the cursor. A vanilla list keeps the wheel for itself:
/// `AbstractContainerWidget` inherits `AbstractScrollArea.mouseScrolled`, which
/// scrolls and never reaches the list's rows. A cycle button outside a list is
/// the screen's own child, so it takes the step (`CycleButton.mouseScrolled`: up
/// goes back). `AbstractSliderButton` does not override it, so a slider outside
/// a list does nothing.
///
/// **Horizon improvement (diverges from Vanilla 26.3):** a slider or cycle
/// button focused by the keyboard (Tab, the arrows, a pad) steps or cycles on
/// the wheel wherever the cursor is. Vanilla 26.3 does not intercept the wheel
/// for sliders at all; this makes keyboard navigation far more usable. Focus
/// left by a click does not count (`focus_from_keyboard`): vanilla's wheel
/// ignores it, and honouring it toggled the clicked option on every later wheel
/// event, from anywhere on the screen, while the list never scrolled.
/// **Horizon extra (diverges from Vanilla 26.3, off by default):** "Scroll wheel
/// changes settings" lets a hovered list row take the step too, and a slider or
/// cycle button focused by the keyboard (Tab, the arrows, a pad) step on the
/// wheel wherever the cursor is. Focus left by a click does not count
/// (`focus_from_keyboard`): honouring it toggled the clicked option on every
/// later wheel event, from anywhere on the screen, while the list never scrolled.
/// The graphics screen has its own wheel (`GraphicsUi::wheel`).
pub fn wheel(
&mut self,
@@ -4527,17 +4785,17 @@ impl OptionsUi {
}
let plain = self.resource_error.is_none()
&& !(self.pack_screen.is_some() && self.stack.last() == Some(&Page::Packs));
// The "Scroll wheel changes settings" extra off: the wheel only scrolls.
if !plain || !s.extras.wheel_cycles_settings {
if !plain {
self.scroll_at(s, size, cursor, amount);
return Vec::new();
}
// Horizon improvement: a keyboard-focused slider or cycle button
// intercepts the wheel before hover detection (see doc-comment above).
// A click focuses its widget for the highlight too, and vanilla's wheel
// must not act on that: it would toggle the clicked option from anywhere
// on the screen and the list would never scroll.
if amount != 0.0 && self.focus_from_keyboard {
let extra = s.extras.wheel_cycles_settings;
// The extra: a keyboard-focused slider or cycle button intercepts the
// wheel before hover detection (see doc-comment above). A click focuses
// its widget for the highlight too, and the wheel must not act on that:
// it would toggle the clicked option from anywhere on the screen and the
// list would never scroll.
if extra && amount != 0.0 && self.focus_from_keyboard {
let (widgets, _, _, _) = self.layout(s, size.0, size.1);
match &self.focus {
Some(FocusId::Slider(opt)) => {
@@ -4566,25 +4824,22 @@ impl OptionsUi {
Extra(crate::extras::Item),
}
let (widgets, top, bottom, _) = self.layout(s, size.0, size.1);
// A list's row takes the wheel only with the extra on.
let takes_wheel = |wd: &Widget| {
ui::hovered(cursor, wd.x, wd.y, wd.w, wd.h)
&& (!wd.list || (extra && cursor.1 >= top && cursor.1 < bottom))
};
let hovered = widgets.iter().find_map(|wd| match &wd.kind {
WKind::Button {
active: true,
act: Act::Cycle(opt),
..
} if ui::hovered(cursor, wd.x, wd.y, wd.w, wd.h)
&& !(wd.list && (cursor.1 < top || cursor.1 >= bottom)) =>
{
Some(HoveredControl::Cycle(*opt))
}
} if takes_wheel(wd) => Some(HoveredControl::Cycle(*opt)),
WKind::Button {
active: true,
act: Act::Extra(item),
..
} if ui::hovered(cursor, wd.x, wd.y, wd.w, wd.h)
&& !(wd.list && (cursor.1 < top || cursor.1 >= bottom)) =>
{
Some(HoveredControl::Extra(*item))
}
} if takes_wheel(wd) => Some(HoveredControl::Extra(*item)),
_ => None,
});
match hovered {
@@ -5202,6 +5457,9 @@ impl OptionsUi {
if p == Page::Music {
events.push(Event::ListMusicTracks);
}
if p == Page::ModBrowser {
events.push(Event::BrowseMods);
}
self.push(p);
}
Act::Done => {
@@ -5259,10 +5517,26 @@ impl OptionsUi {
}
Act::NativeButton(i, b) => events.push(Event::NativeButton(i, b)),
Act::OpenModFolder => events.push(Event::OpenModFolder),
Act::ModProject(i) => {
if let Some(listing) = self.mod_browser.listings.get(i) {
self.mod_project = listing.id.clone();
self.push(Page::ModProject);
}
}
Act::InstallMod => {
self.mod_browser.message = None;
events.push(Event::InstallMod(self.mod_project.clone()));
}
Act::OpenModPage => {
if let Some(l) = self.mod_browser.listing(&self.mod_project) {
events.push(Event::OpenModPage(l.page.clone()));
}
}
Act::PvpPreset => {
crate::pvp::apply_preset(s);
events.push(Event::Changed);
}
Act::HudEditor => events.push(Event::HudEditor),
Act::AccessibilityHelp => events.push(Event::AccessibilityHelp),
Act::PickShaderPack(i) => {
if let Some(pack) = s.shader_packs.get(i) {
@@ -7549,9 +7823,10 @@ mod tests {
/// A content mod's set builds the block-state tables an open world's
/// chunks were decoded with, so its toggle freezes in-world; a native
/// mod gates live through `native_on` and stays available.
/// mod gates live through `native_on` and one of assets and data reloads
/// live, so they stay available.
#[test]
fn in_a_world_only_native_mod_toggles_stay_active() {
fn in_a_world_only_live_mod_toggles_stay_active() {
let mut s = Settings::default();
let installed =
|id: &str, native: Option<crate::mod_list::NativePage>| crate::mod_list::Installed {
@@ -7585,6 +7860,11 @@ mod tests {
assert_eq!(toggles(&mut ui, &s), [(0, true), (1, true)]);
ui.open_at(true, Page::Mods);
assert_eq!(toggles(&mut ui, &s), [(0, false), (1, true)]);
// A content mod of assets and data only reloads live in a world this
// game serves (`mods_live`, filled by the game).
s.mods.installed.push(installed("data_only", None));
ui.mods_live.insert("data_only".into());
assert_eq!(toggles(&mut ui, &s), [(0, false), (1, true), (2, true)]);
}
/// An Extras or PvP & HUD label wider than half a row takes the whole row, so every label
@@ -8044,7 +8324,8 @@ mod tests {
let mut s = Settings::default();
let mut ui = OptionsUi::default();
// 1. Extras page: scrolling wheel on Sort switch cycles it
// 1. Extras page: with the wheel extra on, the wheel on the Sort switch cycles it
s.extras.wheel_cycles_settings = true;
ui.show(Page::Extras);
let size = (480.0, 600.0);
let (widgets, _, _, _) = ui.layout(&s, size.0, size.1);
@@ -8108,9 +8389,9 @@ mod tests {
assert_eq!(s.horizon.chunk_updates, 11);
}
/// Vanilla's wheel scrolls the list (`AbstractScrollArea.mouseScrolled`); only a
/// widget focused by the keyboard takes the step from anywhere on the screen.
/// A click focuses its widget for the highlight, and honouring that focus made a
/// Vanilla's wheel scrolls the list (`AbstractScrollArea.mouseScrolled`); with the
/// extra on, a widget focused by the keyboard takes the step from anywhere on the
/// screen. A click focuses its widget for the highlight, and honouring that focus made a
/// clicked option toggle on every later wheel event while the list never scrolled.
#[test]
fn a_clicked_option_lets_the_wheel_scroll_the_list() {
@@ -8172,8 +8453,9 @@ mod tests {
assert!(!events.contains(&Event::Changed));
assert!(ui.scroll > start, "the list scrolls down instead");
// Keyboard focus keeps the documented divergence: the wheel steps it
// wherever the cursor is, even off the widget.
// With the extra on, keyboard focus keeps the documented divergence: the
// wheel steps it wherever the cursor is, even off the widget.
s.extras.wheel_cycles_settings = true;
ui.focus = Some(FocusId::Cycle(Opt::ViewBobbing));
ui.focus_from_keyboard = true;
let events = ui.wheel(&mut s, size, cursor, framebuffer, -1.0);
@@ -8183,7 +8465,7 @@ mod tests {
}
#[test]
fn the_wheel_only_scrolls_with_the_extra_off() {
fn the_wheel_scrolls_a_list_past_its_cycle_buttons_with_the_extra_off() {
let mut s = Settings::default();
let mut ui = OptionsUi::default();
ui.show(Page::Video);
@@ -8194,20 +8476,22 @@ mod tests {
content > bottom - top,
"the video page scrolls at this size"
);
let (opt, over) = widgets
let (opt, over, list) = widgets
.iter()
.find_map(|wd| match wd.kind {
WKind::Button {
act: Act::Cycle(opt),
..
} if !wd.list || (wd.y >= top && wd.y + wd.h <= bottom) => {
Some((opt, (wd.x + 5.0, wd.y + 5.0)))
} if wd.y >= top && wd.y + wd.h <= bottom => {
Some((opt, (wd.x + 5.0, wd.y + 5.0), wd.list))
}
_ => None,
})
.expect("a visible cycle button");
s.extras.wheel_cycles_settings = false;
assert!(list, "{opt:?} is a row of the video list");
// The default, as in vanilla (#9633).
assert!(!s.extras.wheel_cycles_settings);
let before = format!("{s:?}");
let start = ui.scroll;
let events = ui.wheel(&mut s, size, over, framebuffer, -1.0);
@@ -8223,7 +8507,7 @@ mod tests {
ui.wheel(&mut s, size, over, framebuffer, -1.0);
assert_eq!(format!("{s:?}"), before);
// On (the default), vanilla's CycleButton.mouseScrolled again.
// On, the row under the cursor takes the step.
s.extras.wheel_cycles_settings = true;
let before = format!("{s:?}");
let events = ui.wheel(&mut s, size, over, framebuffer, -1.0);
@@ -8249,6 +8533,7 @@ mod tests {
Opt::NotificationDisplayTime,
Opt::PadMoveDeadzone,
Opt::PadLookDeadzone,
Opt::PadTriggerThreshold,
Opt::SprintWindow,
];
for opt in stepped_opts {
+11 -39
View File
@@ -1,43 +1,13 @@
//! The controller's menu cursor in inventories: a crosshair whose centre is
//! the point it clicks. Horizon's own art (pad_cursor.png).
//! the point it clicks. Horizon's own art, packed with the button glyphs (`pad_glyph`).
/// The atlas sprite.
pub const SPRITE: &str = "horizon/pad_cursor";
/// The texture's size in texels, and the size it's drawn at in GUI pixels
/// (an item's).
pub const SIZE: u32 = 32;
pub const SPRITE: &str = "horizon:gui/controller/crosshair_inv";
/// The size it's drawn at in GUI pixels (an item's); its texture is twice that.
pub const DRAWN: f32 = 16.0;
const PNG: &[u8] = include_bytes!("pad_cursor.png");
/// The texture as `SIZE` square RGBA texels.
pub fn pixels() -> Vec<u8> {
decode(PNG).expect("pad_cursor.png is a 32x32 PNG")
}
fn decode(bytes: &[u8]) -> Option<Vec<u8>> {
let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
let mut reader = decoder.read_info().ok()?;
let info = reader.info();
if (info.width, info.height) != (SIZE, SIZE) {
return None;
}
let mut buf = vec![0; reader.output_buffer_size()?];
let frame = reader.next_frame(&mut buf).ok()?;
let channels = frame.color_type.samples();
Some(
buf.chunks(channels)
.take((SIZE * SIZE) as usize)
.flat_map(|p| match channels {
4 => [p[0], p[1], p[2], p[3]],
3 => [p[0], p[1], p[2], 255],
2 => [p[0], p[0], p[0], p[1]],
_ => [p[0], p[0], p[0], 255],
})
.collect(),
)
}
pub(crate) const PNG: &[u8] =
include_bytes!("../../horizon-client/assets/horizon/textures/gui/controller/crosshair_inv.png");
/// Where to draw the cursor (top-left, GUI pixels) so its centre lands on
/// the GUI point `at`.
@@ -51,10 +21,12 @@ mod tests {
#[test]
fn the_cursor_is_a_crosshair_centred_on_its_point() {
let px = pixels();
assert_eq!(px.len(), (SIZE * SIZE * 4) as usize);
let alpha = |x: u32, y: u32| px[((y * SIZE + x) * 4 + 3) as usize];
let middle = (SIZE / 2 - 1)..=(SIZE / 2);
let (_, w, h, px) = crate::pad_glyph::images()
.find(|i| i.0 == SPRITE)
.expect("the cursor is packed");
assert_eq!((w as f32, h as f32), (DRAWN * 2.0, DRAWN * 2.0));
let alpha = |x: u32, y: u32| px[((y * w + x) * 4 + 3) as usize];
let middle = (w / 2 - 1)..=(w / 2);
assert!(
middle
.clone()
@@ -0,0 +1,386 @@
//! A controller button as the player sees it: Horizon's own art where the pad's family has one
//! (`assets/horizon/textures/gui/controller/`, which a resource pack can replace), else the label
//! printed on the pad, in its colour, on a dark key. Whatever shows a button draws and measures it
//! here; `Button::label` stays for plain text and narration.
use crate::gamepad::{Button, GlyphStyle};
use crate::hud::{HudAtlas, HudBatch, rgba};
use crate::pad_cursor;
/// The art's height as drawn, in GUI pixels. Its texture is twice that, so it maps pixel for pixel
/// at GUI scale 2 and to whole pixels at 4.
const DRAWN_HEIGHT: f32 = 16.0;
/// The clear edge the art has all round.
const MARGIN: f32 = 2.0;
/// How tall a glyph shows, art or key: the text line and 2 above and below it.
pub const HEIGHT: f32 = DRAWN_HEIGHT - 2.0 * MARGIN;
/// How wide a face button's art shows; a declared button's label leaves it room.
pub const FACE_WIDTH: f32 = 12.0;
/// A picture of a button in a family (Auto reads as Xbox, as `Button::label` has it). Every family
/// has its own file for every button, some of them a copy of another's until theirs is drawn.
struct Art {
family: GlyphStyle,
button: Button,
/// The atlas sprite: where a pack's own file lands.
sprite: &'static str,
png: &'static [u8],
}
macro_rules! art {
($($family:ident $button:ident $file:literal,)*) => {
&[$(Art {
family: GlyphStyle::$family,
button: Button::$button,
sprite: concat!("horizon:gui/controller/", $file),
png: include_bytes!(concat!(
"../../horizon-client/assets/horizon/textures/gui/controller/",
$file,
".png"
)),
},)*]
};
}
/// All the art. A new picture is one line here and its file: a button not listed is text, and a
/// picture is measured from its texture, so it can be as wide as it needs.
static ART: &[Art] = art![
Xbox South "xb_a",
Xbox East "xb_b",
Xbox West "xb_x",
Xbox North "xb_y",
Xbox LeftBumper "xb_lb",
Xbox RightBumper "xb_rb",
Xbox LeftTrigger "xb_lt",
Xbox RightTrigger "xb_rt",
Xbox Select "xb_view",
Xbox Start "xb_menu",
Xbox LeftStick "xb_l3",
Xbox RightStick "xb_r3",
Xbox DPadUp "xb_up",
Xbox DPadDown "xb_down",
Xbox DPadLeft "xb_left",
Xbox DPadRight "xb_right",
PlayStation South "psx_cross",
PlayStation East "psx_circle",
PlayStation West "psx_square",
PlayStation North "psx_triangle",
PlayStation LeftBumper "psx_l1",
PlayStation RightBumper "psx_r1",
PlayStation LeftTrigger "psx_l2",
PlayStation RightTrigger "psx_r2",
PlayStation Select "psx_create",
PlayStation Start "psx_options",
PlayStation LeftStick "psx_l3",
PlayStation RightStick "psx_r3",
PlayStation DPadUp "psx_up",
PlayStation DPadDown "psx_down",
PlayStation DPadLeft "psx_left",
PlayStation DPadRight "psx_right",
Nintendo South "nx_b",
Nintendo East "nx_a",
Nintendo West "nx_y",
Nintendo North "nx_x",
Nintendo LeftBumper "nx_l",
Nintendo RightBumper "nx_r",
Nintendo LeftTrigger "nx_zl",
Nintendo RightTrigger "nx_zr",
Nintendo Select "nx_minus",
Nintendo Start "nx_plus",
Nintendo LeftStick "nx_l3",
Nintendo RightStick "nx_r3",
Nintendo DPadUp "nx_up",
Nintendo DPadDown "nx_down",
Nintendo DPadLeft "nx_left",
Nintendo DPadRight "nx_right",
];
fn find(button: Button, style: GlyphStyle) -> Option<&'static Art> {
let family = match style {
GlyphStyle::Auto => GlyphStyle::Xbox,
style => style,
};
ART.iter()
.find(|art| art.family == family && art.button == button)
}
/// The sprite of a button in a family, if it has one.
pub fn sprite(button: Button, style: GlyphStyle) -> Option<&'static str> {
find(button, style).map(|art| art.sprite)
}
/// The sprite this atlas can draw for the button; none (the boot screen's atlas) means text.
pub fn art(atlas: &HudAtlas, button: Button, style: GlyphStyle) -> Option<&'static str> {
sprite(button, style).filter(|name| atlas.sprites.contains_key(*name))
}
/// How wide the art shows: half its texture (IHDR's width, in a PNG's first bytes) less its edge.
/// A pack's own texture of another size is drawn in the same place.
fn art_width(art: &Art) -> f32 {
let texels = u32::from_be_bytes([art.png[16], art.png[17], art.png[18], art.png[19]]);
texels as f32 / 2.0 - 2.0 * MARGIN
}
/// How wide the glyph shows.
pub fn width(atlas: &HudAtlas, button: Button, style: GlyphStyle) -> f32 {
match find(button, style).filter(|a| atlas.sprites.contains_key(a.sprite)) {
Some(art) => art_width(art),
None => HudBatch::text_width(atlas, button.label(style)).ceil() + 4.0,
}
}
/// What the glyph covers with its label's text at `(x, y)`: `[x, y, w, h]`.
pub fn rect(atlas: &HudAtlas, button: Button, style: GlyphStyle, x: f32, y: f32) -> [f32; 4] {
[x - 1.0, y - 2.0, width(atlas, button, style), HEIGHT]
}
/// Draw the glyph over the 8 pixel text line at `(x, y)`, filling [`rect`]. A dark key backs text
/// only: the art brings its own.
pub fn draw(
batch: &mut HudBatch,
atlas: &HudAtlas,
button: Button,
style: GlyphStyle,
x: f32,
y: f32,
) {
let [left, top, wide, tall] = rect(atlas, button, style, x, y);
if let Some(name) = art(atlas, button, style) {
batch.sprite_scaled(
atlas,
name,
left - MARGIN,
top - MARGIN,
wide + 2.0 * MARGIN,
tall + 2.0 * MARGIN,
u32::MAX,
);
} else {
let [r, g, b] = button.color(style);
batch.fill(atlas, left, top, wide, tall, rgba(40, 40, 40, 255));
batch.text(
atlas,
button.label(style),
x + 1.0,
y,
rgba(r, g, b, 255),
false,
);
}
}
/// Horizon's art for the atlas: each `(sprite, width, height, RGBA texels)`.
pub fn images() -> impl Iterator<Item = (String, u32, u32, Vec<u8>)> {
ART.iter()
.map(|art| (art.sprite, art.png))
.chain([(pad_cursor::SPRITE, pad_cursor::PNG)])
.map(|(name, png)| {
let (w, h, pixels) = decode(png).expect("Horizon's controller art is PNG");
(name.to_owned(), w, h, pixels)
})
}
fn decode(bytes: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
let mut reader = decoder.read_info().ok()?;
let (w, h) = (reader.info().width, reader.info().height);
let mut buf = vec![0; reader.output_buffer_size()?];
let frame = reader.next_frame(&mut buf).ok()?;
let channels = frame.color_type.samples();
let pixels = buf
.chunks(channels)
.take((w * h) as usize)
.flat_map(|p| match channels {
4 => [p[0], p[1], p[2], p[3]],
3 => [p[0], p[1], p[2], 255],
2 => [p[0], p[0], p[0], p[1]],
_ => [p[0], p[0], p[0], 255],
})
.collect();
Some((w, h, pixels))
}
#[cfg(test)]
mod tests {
use super::*;
fn atlas() -> HudAtlas {
HudAtlas::build(std::path::Path::new("/nonexistent/horizon-test-assets")).unwrap()
}
fn file(button: Button, style: GlyphStyle) -> Option<&'static str> {
sprite(button, style).and_then(|name| name.strip_prefix("horizon:gui/controller/"))
}
/// The batch's quads as `[left, top, right, bottom]`.
fn quads(batch: &HudBatch) -> Vec<[f32; 4]> {
batch
.vertices
.as_chunks::<6>()
.0
.iter()
.map(|quad| {
let xs = quad.iter().map(|v| v.pos[0]);
let ys = quad.iter().map(|v| v.pos[1]);
[
xs.clone().fold(f32::MAX, f32::min),
ys.clone().fold(f32::MAX, f32::min),
xs.fold(f32::MIN, f32::max),
ys.fold(f32::MIN, f32::max),
]
})
.collect()
}
#[test]
fn xbox_and_auto_show_the_letters_and_shoulders() {
for style in [GlyphStyle::Xbox, GlyphStyle::Auto] {
for (button, expected) in [
(Button::South, "xb_a"),
(Button::East, "xb_b"),
(Button::West, "xb_x"),
(Button::North, "xb_y"),
(Button::LeftBumper, "xb_lb"),
(Button::RightBumper, "xb_rb"),
(Button::LeftTrigger, "xb_lt"),
(Button::RightTrigger, "xb_rt"),
(Button::Select, "xb_view"),
(Button::Start, "xb_menu"),
] {
assert_eq!(file(button, style), Some(expected));
}
}
}
#[test]
fn playstation_shows_its_symbols_by_position_and_its_own_shoulders() {
let style = GlyphStyle::PlayStation;
assert_eq!(file(Button::South, style), Some("psx_cross"));
assert_eq!(file(Button::East, style), Some("psx_circle"));
assert_eq!(file(Button::West, style), Some("psx_square"));
assert_eq!(file(Button::North, style), Some("psx_triangle"));
assert_eq!(file(Button::LeftBumper, style), Some("psx_l1"));
assert_eq!(file(Button::RightTrigger, style), Some("psx_r2"));
assert_eq!(file(Button::Select, style), Some("psx_create"));
}
#[test]
fn nintendo_shows_what_its_pad_prints_at_each_position() {
let style = GlyphStyle::Nintendo;
assert_eq!(file(Button::South, style), Some("nx_b"));
assert_eq!(file(Button::East, style), Some("nx_a"));
assert_eq!(file(Button::West, style), Some("nx_y"));
assert_eq!(file(Button::North, style), Some("nx_x"));
assert_eq!(file(Button::LeftTrigger, style), Some("nx_zl"));
assert_eq!(file(Button::Start, style), Some("nx_plus"));
}
#[test]
fn every_family_has_its_own_file_for_every_button() {
for (style, prefix) in [
(GlyphStyle::Xbox, "xb_"),
(GlyphStyle::PlayStation, "psx_"),
(GlyphStyle::Nintendo, "nx_"),
] {
for button in Button::ALL {
let name = file(button, style).unwrap();
assert!(name.starts_with(prefix), "{button:?} {name}");
}
}
}
#[test]
fn every_sprite_has_its_texture_at_twice_gui_size() {
let images: Vec<_> = images().collect();
assert_eq!(images.len(), ART.len() + 1, "the art and the cursor");
for (name, w, h, pixels) in &images {
assert_eq!(pixels.len(), (w * h * 4) as usize, "{name}");
assert_eq!(*h as f32, DRAWN_HEIGHT * 2.0, "{name}");
}
for art in ART {
let (_, w, ..) = images.iter().find(|i| i.0 == art.sprite).unwrap();
assert_eq!(art_width(art), *w as f32 / 2.0 - 4.0, "{}", art.sprite);
}
}
#[test]
fn no_button_has_two_pictures_in_a_family() {
for (i, art) in ART.iter().enumerate() {
assert!(
!ART[..i]
.iter()
.any(|a| a.family == art.family && a.button == art.button),
"{}",
art.sprite
);
}
}
#[test]
fn art_measures_from_its_shape_and_text_from_its_label() {
let atlas = atlas();
assert_eq!(width(&atlas, Button::South, GlyphStyle::Xbox), 12.0);
assert_eq!(width(&atlas, Button::LeftBumper, GlyphStyle::Xbox), 20.0);
assert_eq!(
width(&atlas, Button::LeftBumper, GlyphStyle::PlayStation),
20.0
);
assert_eq!(width(&atlas, Button::DPadUp, GlyphStyle::Nintendo), 12.0);
let mut no_art = atlas;
no_art
.sprites
.remove(sprite(Button::LeftTrigger, GlyphStyle::Nintendo).unwrap());
let label = HudBatch::text_width(&no_art, "ZL").ceil() + 4.0;
assert_eq!(
width(&no_art, Button::LeftTrigger, GlyphStyle::Nintendo),
label
);
}
#[test]
fn an_atlas_without_the_art_falls_back_to_text() {
let mut atlas = atlas();
atlas
.sprites
.remove(sprite(Button::South, GlyphStyle::Xbox).unwrap());
assert_eq!(art(&atlas, Button::South, GlyphStyle::Xbox), None);
let label = HudBatch::text_width(&atlas, "A").ceil() + 4.0;
assert_eq!(width(&atlas, Button::South, GlyphStyle::Xbox), label);
}
#[test]
fn art_is_one_quad_centred_on_the_text_line_with_no_key_behind_it() {
let atlas = atlas();
let mut batch = HudBatch::new([640, 480]);
draw(
&mut batch,
&atlas,
Button::South,
GlyphStyle::Xbox,
10.0,
20.0,
);
assert_eq!(quads(&batch), vec![[7.0, 16.0, 23.0, 32.0]]);
let mut batch = HudBatch::new([640, 480]);
draw(
&mut batch,
&atlas,
Button::LeftBumper,
GlyphStyle::Xbox,
10.0,
20.0,
);
assert_eq!(quads(&batch), vec![[7.0, 16.0, 31.0, 32.0]]);
}
#[test]
fn text_sits_on_a_key_covering_what_rect_says() {
let mut atlas = atlas();
let style = GlyphStyle::Nintendo;
atlas.sprites.remove(sprite(Button::South, style).unwrap());
let mut batch = HudBatch::new([640, 480]);
draw(&mut batch, &atlas, Button::South, style, 10.0, 20.0);
let [x, y, w, h] = rect(&atlas, Button::South, style, 10.0, 20.0);
assert_eq!(quads(&batch)[0], [x, y, x + w, y + h]);
}
}
+126 -2
View File
@@ -751,7 +751,38 @@ impl ChatOptions {
}
}
#[derive(Clone, Debug, Default, PartialEq)]
/// Combat feel (`docs/pvp.md`): how fights look. Legacy draws them as 1.8.9 did; what the
/// game sends to a server never changes, so a vanilla server and its anticheat see 26.3.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Combat {
/// Legacy (1.8.9) instead of Modern.
pub legacy: bool,
/// Keep the attack indicator under Legacy (1.8.9 had none).
pub indicator: bool,
/// Under Legacy, silence the attack sounds 1.9 added (1.8.9 played none on a hit).
pub sounds: bool,
/// The worlds this game hosts (single player and LAN) fight by 1.8.9's rules: no attack
/// cooldown, no sweep, 1.8 weapon damage, effects, enchantments and critical hits
/// (`horizon_client::legacy_combat`). Never used as a client of a server.
pub rules: bool,
}
impl Combat {
/// Whether this client leaves out sound `name` a server sent: under Legacy with its
/// sounds, the `entity.player.attack.*` family (strong, weak, crit, sweep, knockback,
/// nodamage), which 1.9 brought with the attack cooldown.
pub fn mutes_sound(&self, name: &str) -> bool {
let name = name.strip_prefix("minecraft:").unwrap_or(name);
self.legacy && self.sounds && name.starts_with("entity.player.attack.")
}
/// Whether this client leaves out particles of `ty` a server sent: under Legacy, the
/// sweep and the damage hearts, which 1.9 brought (1.8.9's hits show crits only).
pub fn hides_particle(&self, ty: crate::particles::ParticleType) -> bool {
use crate::particles::ParticleType as T;
self.legacy && matches!(ty, T::SweepAttack | T::DamageIndicator)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pvp {
pub crosshair: Crosshair,
pub outline: Outline,
@@ -761,6 +792,26 @@ pub struct Pvp {
pub chat: ChatOptions,
/// Where each HUD widget sits and its size.
pub layout: crate::pvp_hud::Layout,
/// The key that opens the HUD editor from the game (unbound at first).
pub editor_key: String,
pub combat: Combat,
}
/// The HUD editor key's name for the controls (`Extras::set_key`).
pub const HUD_EDITOR_KEY: &str = "horizon.hud_editor";
impl Default for Pvp {
fn default() -> Self {
Self {
crosshair: Crosshair::default(),
outline: Outline::default(),
hit: Hit::default(),
hud: Hud::default(),
tweaks: Tweaks::default(),
chat: ChatOptions::default(),
layout: crate::pvp_hud::Layout::default(),
editor_key: "key.keyboard.unknown".into(),
combat: Combat::default(),
}
}
}
fn flag(value: &str, d: bool) -> bool {
match value {
@@ -822,6 +873,7 @@ impl Pvp {
}
"hudShadow" => u.shadow = flag(value, u.shadow),
"hudPlace" => self.layout.parse(value),
"hudEditorKey" if value.starts_with("key.") => self.editor_key = value.to_owned(),
"hudPing" => u.ping = flag(value, u.ping),
"hudPingCorner" => u.ping_corner = Corner::parse(value, u.ping_corner),
"hudCoords" => u.coords = flag(value, u.coords),
@@ -858,6 +910,14 @@ impl Pvp {
"chatCompact" => self.chat.compact = flag(value, self.chat.compact),
"chatCopy" => self.chat.copy = flag(value, self.chat.copy),
"chatSearch" => self.chat.search = flag(value, self.chat.search),
"combatFeel" => match value {
"legacy" => self.combat.legacy = true,
"modern" => self.combat.legacy = false,
_ => {}
},
"combatIndicator" => self.combat.indicator = flag(value, self.combat.indicator),
"combatSounds" => self.combat.sounds = flag(value, self.combat.sounds),
"combatRules" => self.combat.rules = flag(value, self.combat.rules),
"viewTime" => {
t.time = match value {
"off" => None,
@@ -957,6 +1017,7 @@ impl Pvp {
u.shadow,
);
self.layout.serialize(out);
let _ = writeln!(out, "hudEditorKey:{}", self.editor_key);
let t = &self.tweaks;
let _ = write!(
out,
@@ -981,6 +1042,13 @@ impl Pvp {
"chatTimestamps:{}\nchatCompact:{}\nchatCopy:{}\nchatSearch:{}\n",
c.timestamps, c.compact, c.copy, c.search,
);
let c = &self.combat;
let feel = if c.legacy { "legacy" } else { "modern" };
let _ = write!(
out,
"combatFeel:{feel}\ncombatIndicator:{}\ncombatSounds:{}\ncombatRules:{}\n",
c.indicator, c.sounds, c.rules
);
}
}
@@ -1093,6 +1161,8 @@ opts! {
HudTextColour, "hud_text_colour", "Text colour",
"The widgets' text colour. Durability keeps its green to red.";
HudShadow, "hud_shadow", "Text shadow", "A shadow under the widgets' text, as vanilla's HUD text has.";
HudEditorKey, "hud_editor_key", "HUD editor key",
"Opens the HUD editor from the game. Click, then press a key; Escape unbinds it.";
NumericEnchants, "numeric_enchants", "Enchantment levels as numbers",
"Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.";
FireLower, "fire_lower", "Lower fire",
@@ -1117,6 +1187,14 @@ opts! {
"Clicking a message in the open chat copies its text, unless the click does something else.";
ChatSearch, "chat_search", "Chat search",
"Ctrl+F in the open chat: what you type shows only the messages holding it. Enter or Ctrl+F again stops.";
CombatFeel, "combat_feel", "Combat feel",
"Legacy draws fights as 1.8.9 did: the item in your hand, its swing, eating, the bow and a sword held up while you hold use, without the dip after each hit or the attack indicator. Only the look: what you send a server stays the same.";
CombatIndicator, "combat_indicator", "Attack indicator in Legacy",
"Keeps the attack indicator under Legacy, which 1.8.9 didn't have.";
CombatSounds, "combat_sounds", "1.8 hit sounds",
"Under Legacy, leaves out the attack sounds 1.9 added (strong, weak, sweep, critical...). Hurt sounds stay.";
CombatRules, "combat_rules", "1.8 combat in worlds you host",
"Single player and the worlds you open to LAN fight by 1.8.9's rules: no attack cooldown or sweep, 1.8 weapon damage, Strength, Sharpness and critical hits. Servers you join keep their own rules.";
}
/// The PvP & HUD page: headers (language keys) and options.
pub fn rows() -> Vec<Result<Opt, &'static str>> {
@@ -1183,6 +1261,7 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
HudOpacity,
HudTextColour,
HudShadow,
HudEditorKey,
]
.map(Ok),
);
@@ -1195,6 +1274,8 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
rows.extend([ViewTime, ViewWeather].map(Ok));
rows.push(Err("horizon.pvp.header.chat"));
rows.extend([ChatTimestamps, ChatCompact, ChatCopy, ChatSearch].map(Ok));
rows.push(Err("horizon.pvp.header.combat"));
rows.extend([CombatFeel, CombatIndicator, CombatSounds, CombatRules].map(Ok));
rows
}
impl Opt {
@@ -1207,7 +1288,7 @@ impl Opt {
HitColour | HitOpacity => Some(Hit),
HudArmour | HudEffects | HudHeld | HudPing | HudCoords | HudServer | HudCombo
| HudFps | HudCps | HudKeys | HudClock | HudReach | HudSpeed | HudBackground
| HudTextColour | HudShadow | NumericEnchants => None,
| HudTextColour | HudShadow | HudEditorKey | NumericEnchants => None,
HudOpacity => Some(HudBackground),
HudFpsCorner => Some(HudFps),
HudCpsCorner => Some(HudCps),
@@ -1218,6 +1299,7 @@ impl Opt {
FireLower | ShieldSize | TotemSize | GlintColour | ClearWater | ClearLava => None,
ViewTime | ViewWeather => None,
ChatTimestamps | ChatCompact | ChatCopy | ChatSearch => None,
CombatFeel | CombatIndicator | CombatSounds | CombatRules => None,
HudPingCorner => Some(HudPing),
HudCoordsCorner => Some(HudCoords),
HudServerCorner => Some(HudServer),
@@ -1259,6 +1341,9 @@ impl Opt {
Opt::ChatCompact => p.chat.compact,
Opt::ChatCopy => p.chat.copy,
Opt::ChatSearch => p.chat.search,
Opt::CombatIndicator => p.combat.indicator,
Opt::CombatSounds => p.combat.sounds,
Opt::CombatRules => p.combat.rules,
_ => return None,
})
}
@@ -1299,6 +1384,7 @@ impl Opt {
Opt::HudSpeedCorner => corner_label(p.hud.speed_corner),
Opt::HudOpacity => format!("{}%", p.hud.opacity),
Opt::HudTextColour => p.hud.text.label(),
Opt::HudEditorKey => crate::keybinds::display_name(&p.editor_key).plain(),
Opt::FireLower => match p.tweaks.fire_lower {
0 => crate::lang::get_or("Off", "Off"),
v => format!("{v}%"),
@@ -1309,6 +1395,10 @@ impl Opt {
Some(colour) => colour.label(),
None => crate::lang::get_or("Off", "Off"),
},
Opt::CombatFeel => match p.combat.legacy {
true => crate::lang::get_or("horizon.pvp.combat_feel.legacy", "Legacy (1.8.9)"),
false => crate::lang::get_or("horizon.pvp.combat_feel.modern", "Modern"),
},
Opt::ViewTime => p.tweaks.time.map_or_else(world_label, ViewTime::label),
Opt::ViewWeather => p
.tweaks
@@ -1369,6 +1459,8 @@ impl Opt {
Opt::HudOpacity => u.opacity = step_by(u.opacity, HUD_OPACITIES, 10, back),
Opt::HudTextColour => u.text = u.text.step_solid(back),
Opt::HudShadow => u.shadow = !u.shadow,
// Bound through the controls' key capture (`Item::key_name`).
Opt::HudEditorKey => {}
Opt::HudPing => u.ping = !u.ping,
Opt::HudPingCorner => u.ping_corner = corner(u.ping_corner),
Opt::HudCoords => u.coords = !u.coords,
@@ -1406,6 +1498,10 @@ impl Opt {
Opt::ChatCompact => p.chat.compact = !p.chat.compact,
Opt::ChatCopy => p.chat.copy = !p.chat.copy,
Opt::ChatSearch => p.chat.search = !p.chat.search,
Opt::CombatFeel => p.combat.legacy = !p.combat.legacy,
Opt::CombatIndicator => p.combat.indicator = !p.combat.indicator,
Opt::CombatSounds => p.combat.sounds = !p.combat.sounds,
Opt::CombatRules => p.combat.rules = !p.combat.rules,
Opt::ViewWeather => {
let all: Vec<_> = std::iter::once(None)
.chain(ViewWeather::ALL.map(Some))
@@ -1708,4 +1804,32 @@ mod tests {
assert_eq!(en.get(key), Some(*english), "{key}");
}
}
#[test]
fn legacy_leaves_out_what_1_9_added_to_a_hit() {
use crate::particles::ParticleType as T;
let modern = Combat::default();
let legacy = Combat {
legacy: true,
..Combat::default()
};
let sounds = Combat {
sounds: true,
..legacy
};
for name in [
"minecraft:entity.player.attack.strong",
"entity.player.attack.sweep",
"minecraft:entity.player.attack.nodamage",
] {
assert!(sounds.mutes_sound(name), "{name}");
assert!(!legacy.mutes_sound(name), "only with the sounds option");
assert!(!modern.mutes_sound(name));
}
// 1.8.9 had the hurt sound: it stays.
assert!(!sounds.mutes_sound("minecraft:entity.player.hurt"));
assert!(legacy.hides_particle(T::SweepAttack) && legacy.hides_particle(T::DamageIndicator));
assert!(!legacy.hides_particle(T::Crit) && !legacy.hides_particle(T::EnchantedHit));
assert!(!modern.hides_particle(T::SweepAttack));
}
}
+188 -5
View File
@@ -18,7 +18,7 @@ pub fn set_source_volumes(volumes: [f32; 11]) {
pub fn sound_source(event: &str) -> usize {
let e = event.trim_start_matches("minecraft:");
let index = |name: &str| SOUND_SOURCES.iter().position(|n| *n == name).unwrap_or(0);
const HOSTILE: [&str; 25] = [
const HOSTILE: [&str; 34] = [
"zombie",
"skeleton",
"creeper",
@@ -44,6 +44,16 @@ pub fn sound_source(event: &str) -> usize {
"silverfish",
"zombified_piglin",
"wither_skeleton",
"bogged",
"breeze",
"endermite",
"parched",
"warden",
"zombie_villager",
"vex",
"wither",
// Monster's common hurt/death (e.g. Giant) and other shared sounds.
"hostile",
];
if e == "block.potent_sulfur.noxious_gas" {
// PotentSulfurBlock.animateTick, its only caller, plays it as AMBIENT.
@@ -347,7 +357,7 @@ impl Settings {
.parent()
.map(|dir| crate::mod_list::ModList::load(&crate::logging::shared_dir(dir)))
.unwrap_or_default();
let settings = match fs::read(path) {
let mut settings = match fs::read(path) {
Ok(bytes) => {
let text = String::from_utf8_lossy(&bytes);
extras.read_old_options(&extras_text, &text);
@@ -367,6 +377,9 @@ impl Settings {
..Self::default()
},
};
// Horizon's own key bindings live in horizon-extras.txt, not in options.txt.
let emote = settings.extras.emote_key.clone();
settings.keys.set("key.emote", &emote);
settings.describe_for_crashes();
// Options.load: the backend asked for at startup, what a restart is measured from.
Self {
@@ -636,14 +649,17 @@ impl Settings {
pub fn with_option(&self, key: &str, value: &str) -> Result<Self, String> {
let mut file = self.to_file();
file.set_text(key, value)?;
Ok(Self {
let mut next = Self {
horizon: self.horizon.clone(),
controller: self.controller.clone(),
extras: self.extras.clone(),
mods: self.mods.clone(),
video_modes: self.video_modes.clone(),
..Self::from_file(file)
})
};
// Horizon's own bindings aren't in options.txt.
next.keys.set("key.emote", self.keys.get("key.emote"));
Ok(next)
}
/// The `options.txt` text (Options.save).
pub fn serialize(&self) -> String {
@@ -657,7 +673,9 @@ impl Settings {
if let Some(dir) = path.parent() {
self.horizon.save(dir);
self.controller.save(dir);
self.extras.save(dir);
let mut extras = self.extras.clone();
extras.emote_key = self.keys.get("key.emote").to_owned();
extras.save(dir);
self.mods.save(&crate::logging::shared_dir(dir));
}
}
@@ -814,6 +832,7 @@ pub enum Opt {
PadToggleSneak,
PadMoveDeadzone,
PadLookDeadzone,
PadTriggerThreshold,
PadGlyphs,
PadKeyboard,
/// `options.modelPart.*`: one skin layer (index into `options_file::MODEL_PARTS`).
@@ -855,6 +874,57 @@ mod tests {
horizon_narrator::Status::Off
);
}
/// `Monster.getSoundSource`: the verified families and inherited common sounds.
#[test]
fn hostile_families_keep_their_sound_category() {
for (event, source) in [
("entity.zombie.hurt", 5),
("entity.cow.hurt", 6),
("block.stone.break", 4),
("entity.player.hurt", 7),
("entity.arrow.hit", 7),
("music_disc.13", 2),
("item.goat_horn.sound.0", 2),
("weather.rain", 3),
("ambient.cave", 8),
] {
assert_eq!(sound_source(event), source, "{event}");
}
// Explicit sources still take precedence, including H331's arrow route.
let mut audio = crate::audio::Audio::silent(Path::new("/nonexistent/h068-audio"));
audio.capture = Some(Vec::new());
audio.play_on(6, "minecraft:entity.arrow.hit", Some([1.0; 3]), 1.0, 1.0);
audio.play_on(4, "minecraft:entity.warden.hurt", Some([1.0; 3]), 1.0, 1.0);
assert_eq!(
audio
.capture
.as_ref()
.unwrap()
.iter()
.map(|s| s.source)
.collect::<Vec<_>>(),
[6, 4]
);
for family in [
"bogged",
"breeze",
"endermite",
"parched",
"warden",
"zombie_villager",
"vex",
"wither",
"hostile",
] {
for suffix in ["hurt", "death"] {
for prefix in ["", "minecraft:"] {
let event = format!("{prefix}entity.{family}.{suffix}");
assert_eq!(sound_source(&event), 5, "{event}");
}
}
}
}
#[test]
fn three_d_skin_layers_move_from_options_txt_to_the_extras_file() {
// #8091 wrote the switch into options.txt; a player who had it on keeps
@@ -880,6 +950,119 @@ mod tests {
);
let _ = fs::remove_dir_all(&dir);
}
fn scratch(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"horizon-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn options_txt_lists_only_vanillas_key_mappings() {
// Options.keyMappings has the friends key, then social interactions; Horizon's emote key
// (#10092 put it between them) is not in the file.
let out = Settings::default().serialize();
assert!(!out.contains("emote"), "{out}");
assert!(out.contains(
"\nkey_key.friends:key.keyboard.o\nkey_key.socialInteractions:key.keyboard.p\n"
));
let mut s = Settings::default();
s.keys.set("key.emote", "key.keyboard.g");
assert!(!s.serialize().contains("emote"));
}
#[test]
fn the_emote_key_is_kept_in_the_extras_file() {
let dir = scratch("emote-key");
let options = dir.join("options.txt");
// Untouched: no extras file is made for it.
Settings::default().save(&options);
assert!(!dir.join(crate::extras::FILE).exists());
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.b"
);
// Rebound in Controls: saved to horizon-extras.txt, and options.txt stays vanilla's.
let mut s = Settings::load(&options);
s.keys.set("key.emote", "key.keyboard.g");
s.save(&options);
assert!(!fs::read_to_string(&options).unwrap().contains("emote"));
let extras = fs::read_to_string(dir.join(crate::extras::FILE)).unwrap();
assert!(
extras.lines().any(|l| l == "emoteKey:key.keyboard.g"),
"{extras}"
);
let back = Settings::load(&options);
assert_eq!(back.keys.get("key.emote"), "key.keyboard.g");
// horizon-ctl options-set rebuilds from options.txt text and keeps it.
assert_eq!(
back.with_option("fov", "90").unwrap().keys.get("key.emote"),
"key.keyboard.g"
);
// Reset to B and back to the default: the file says so.
let mut s = back;
s.keys.set(
"key.emote",
crate::keybinds::KeyBinds::default_of("key.emote"),
);
s.save(&options);
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.b"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn the_emote_key_moves_from_options_txt_to_the_extras_file() {
// #10092 wrote `key_key.emote` into options.txt; a rebound key is kept, the next
// options.txt is vanilla's again.
let dir = scratch("emote-key-move");
let options = dir.join("options.txt");
fs::write(
&options,
"version:4000\nfov:0.5\nkey_key.emote:key.keyboard.h\nkey_key.jump:key.keyboard.j\n",
)
.unwrap();
let s = Settings::load(&options);
assert_eq!(s.keys.get("key.emote"), "key.keyboard.h");
assert_eq!(s.keys.get("key.jump"), "key.keyboard.j");
s.save(&options);
assert!(!fs::read_to_string(&options).unwrap().contains("emote"));
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.h"
);
// Once horizon-extras.txt has its own line, a stale options.txt one is ignored.
fs::write(dir.join(crate::extras::FILE), "emoteKey:key.keyboard.k\n").unwrap();
fs::write(&options, "version:4000\nkey_key.emote:key.keyboard.h\n").unwrap();
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.k"
);
// A value that is no key is not adopted; an unbound emote key is.
fs::remove_file(dir.join(crate::extras::FILE)).unwrap();
fs::write(&options, "version:4000\nkey_key.emote:banana\n").unwrap();
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.b"
);
fs::write(
&options,
"version:4000\nkey_key.emote:key.keyboard.unknown\n",
)
.unwrap();
assert_eq!(
Settings::load(&options).keys.get("key.emote"),
"key.keyboard.unknown"
);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn an_option_set_keeps_horizon_files() {
let mut s = Settings::parse("fov:70\n");
+197 -15
View File
@@ -22,6 +22,11 @@ thread_local! {
static NOTES: RefCell<Vec<(f32, String)>> = const { RefCell::new(Vec::new()) };
}
/// The most spans a thread holds before they are taken ([`take_raw`]): a worker thread
/// that records spans (`lod_reduce`, `lt_*`) while they are on for every thread is never
/// taken from, so it drops what it held instead of growing for as long as they stay on.
const UNTAKEN_MAX: usize = 1 << 14;
/// The shortest step a [`note`] keeps, in milliseconds.
pub const NOTE_MS: f32 = 10.0;
/// The notes a thread keeps between two [`take_notes`]: its slowest steps.
@@ -53,7 +58,7 @@ pub fn record_here(on: bool) {
/// Whether this thread records spans: all of them on, or [`record_here`].
#[inline]
pub fn recording_here() -> bool {
ENABLED.load(Ordering::Relaxed) || HERE.with(Cell::get)
ENABLED.load(Ordering::Relaxed) || HERE.with(Cell::get) || trace::on()
}
/// The thread's CPU time when spans are on for every thread (`HORIZON_SPIKES`): only they
/// print it.
@@ -101,16 +106,41 @@ impl Drop for Guard {
.and_then(|start| Some((thread_cpu()?, start)))
.map_or(ms, |(end, start)| (end - start).as_secs_f32() * 1000.0);
DEPTH.with(|d| d.set(depth));
SPANS.with(|s| {
s.borrow_mut().push(Span {
name,
ms,
cpu_ms,
depth,
})
trace::span(name, started, ms);
push(Span {
name,
ms,
cpu_ms,
depth,
});
}
}
/// Keeps a finished span for the thread's next [`take_raw`].
fn push(span: Span) {
SPANS.with(|s| {
let mut s = s.borrow_mut();
if s.len() >= UNTAKEN_MAX {
s.clear();
}
s.push(span);
});
}
/// Attach per-tick counts without interpreting them as durations.
pub fn counters(counts: &[(&str, u64)]) {
if !enabled() {
return;
}
NOTES.with(|n| {
n.borrow_mut().push((
0.0,
counts
.iter()
.map(|(name, count)| format!("{name}={count}"))
.collect::<Vec<_>>()
.join(", "),
))
});
}
/// Notes that one step inside a span took `ms` milliseconds doing `what` (one player's
/// packets, say), when this thread records spans and the step took at least [`NOTE_MS`]: a
/// span says where a slow tick went, its notes whose doing it was. Only the slowest few are
@@ -163,13 +193,12 @@ impl Laps {
.zip(*cpu)
.map_or(ms, |(end, start)| (end - start).as_secs_f32() * 1000.0);
let depth = DEPTH.with(Cell::get);
SPANS.with(|s| {
s.borrow_mut().push(Span {
name,
ms,
cpu_ms,
depth,
})
trace::span(name, *started, ms);
push(Span {
name,
ms,
cpu_ms,
depth,
});
(*started, *cpu) = (now, now_cpu);
}
@@ -271,6 +300,159 @@ pub fn describe(spans: &[Span]) -> String {
list.join(", ")
}
/// The run's timeline: every span ([`time`], [`guard`], [`Laps::lap`]) on every thread, as
/// Chrome trace events that Perfetto (ui.perfetto.dev) opens as they are. `HORIZON_TRACE=FILE`
/// writes it from the start; [`start`](trace::start) and [`stop`](trace::stop) write one for a
/// while (`horizon-trace` on a dedicated server's console, `trace` on the control socket). Spans
/// show on their threads where they ran, nested by time, so what ran beside what, and what a
/// thread waited for, can be read off. Off, a span costs one relaxed load more.
pub mod trace {
use std::{
cell::Cell,
fs::File,
io::{BufWriter, Write as _},
path::Path,
sync::{
Mutex, PoisonError,
atomic::{AtomicBool, AtomicU32, Ordering},
},
time::Instant,
};
static ON: AtomicBool = AtomicBool::new(false);
static SINK: Mutex<Option<Sink>> = Mutex::new(None);
/// Counts the traces started: a thread names itself again in each one.
static TRACES: AtomicU32 = AtomicU32::new(0);
static NEXT_THREAD: AtomicU32 = AtomicU32::new(1);
thread_local! {
/// This thread's id in traces (0: none yet).
static THREAD: Cell<u32> = const { Cell::new(0) };
/// The trace this thread last named itself in.
static NAMED_IN: Cell<u32> = const { Cell::new(0) };
}
/// How often a trace's buffer goes to the file, at most: a run that ends without
/// [`stop`] loses no more than this.
const FLUSH_EVERY: std::time::Duration = std::time::Duration::from_secs(1);
struct Sink {
file: BufWriter<File>,
origin: Instant,
/// Until when it records ([`start`]'s `seconds`).
until: Option<Instant>,
flushed: Instant,
/// Events written: the next one is preceded by a comma.
events: u64,
}
impl Sink {
fn event(&mut self, event: std::fmt::Arguments) {
let comma = if self.events == 0 { "" } else { ",\n" };
let _ = write!(self.file, "{comma}{{\"pid\":1,{event}}}");
self.events += 1;
}
}
/// Whether a trace is being written.
#[inline]
pub fn on() -> bool {
ON.load(Ordering::Relaxed)
}
/// Starts a trace written to `path` (replacing a trace being written), for `seconds` or
/// until [`stop`].
pub fn start(path: &Path, seconds: Option<u32>) -> std::io::Result<()> {
let mut file = BufWriter::with_capacity(1 << 18, File::create(path)?);
file.write_all(b"[\n")?;
let now = Instant::now();
let sink = Sink {
file,
origin: now,
until: seconds.map(|s| now + std::time::Duration::from_secs(u64::from(s))),
flushed: now,
events: 0,
};
let mut slot = SINK.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(old) = slot.take() {
close(old);
}
*slot = Some(sink);
TRACES.fetch_add(1, Ordering::Relaxed);
ON.store(true, Ordering::Relaxed);
Ok(())
}
/// Starts the trace `HORIZON_TRACE` names, if it names one.
pub fn start_from_env() {
if let Some(path) = std::env::var_os("HORIZON_TRACE")
&& let Err(e) = start(Path::new(&path), None)
{
log::warn!("Horizon: HORIZON_TRACE: {e}");
}
}
/// Ends the trace being written: the file is complete JSON. The events written so far
/// and whether one was being written.
pub fn stop() -> Option<u64> {
ON.store(false, Ordering::Relaxed);
let sink = SINK.lock().unwrap_or_else(PoisonError::into_inner).take()?;
let events = sink.events;
close(sink);
Some(events)
}
/// Whether the trace being written has run its time ([`start`]'s `seconds`): the caller
/// then [`stop`]s it.
pub fn due() -> bool {
on() && SINK
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.and_then(|s| s.until)
.is_some_and(|until| Instant::now() >= until)
}
fn close(mut sink: Sink) {
let _ = sink.file.write_all(b"\n]\n");
let _ = sink.file.flush();
}
/// A span of this thread from `started`, `ms` long.
pub(super) fn span(name: &'static str, started: Instant, ms: f32) {
if !on() {
return;
}
let tid = THREAD.with(|t| {
if t.get() == 0 {
t.set(NEXT_THREAD.fetch_add(1, Ordering::Relaxed));
}
t.get()
});
let trace = TRACES.load(Ordering::Relaxed);
let mut slot = SINK.lock().unwrap_or_else(PoisonError::into_inner);
let Some(sink) = slot.as_mut() else {
return;
};
if NAMED_IN.with(|n| n.replace(trace)) != trace {
let name = std::thread::current()
.name()
.unwrap_or("unnamed")
.replace(['"', '\\'], "_");
sink.event(format_args!(
r#""ph":"M","name":"thread_name","tid":{tid},"args":{{"name":"{name}"}}"#
));
}
let ts = started.saturating_duration_since(sink.origin).as_secs_f64() * 1e6;
sink.event(format_args!(
r#""ph":"X","name":"{name}","tid":{tid},"ts":{ts:.3},"dur":{:.3}"#,
f64::from(ms) * 1e3
));
let now = Instant::now();
if now.duration_since(sink.flushed) >= FLUSH_EVERY {
let _ = sink.file.flush();
sink.flushed = now;
}
}
}
/// The start's timeline: `HORIZON_STARTUP_TRACE=FILE` writes every named step of
/// the start (resource tables, the renderer, the title, a world's open, a
/// server's join, a dimension change) on whichever thread ran it, one Chrome
@@ -8,7 +8,7 @@ use crate::{
gamepad::{Button as PadButton, GlyphStyle},
graphics_style,
hud::{HudAtlas, HudBatch, rgba},
lang,
lang, pad_glyph,
text::Text,
ui,
};
@@ -411,30 +411,23 @@ impl SplitProfiles {
None => tr("horizon.splitscreen.profiles.move", "Move where you play"),
};
let keys = [PadButton::LeftBumper, PadButton::RightBumper];
let key_width = |b: PadButton| width(b.label(style)) + 4.0;
let key_width = |b: PadButton| pad_glyph::width(atlas, b, style);
let total = keys.iter().map(|&b| key_width(b) + 4.0).sum::<f32>() + 2.0 + width(&words);
let (mut x, y) = (((batch.width - total) / 2.0).floor(), 30.0);
for b in keys {
let [r, g, bl] = b.color(style);
let kw = key_width(b);
// A light edge around the dark key: it stands out on the menu's dark background.
batch.fill(
atlas,
x - 2.0,
y - 3.0,
kw + 2.0,
13.0,
rgba(170, 170, 170, 255),
);
batch.fill(atlas, x - 1.0, y - 2.0, kw, 11.0, rgba(40, 40, 40, 255));
batch.text(
atlas,
b.label(style),
x + 1.0,
y,
rgba(r, g, bl, 255),
false,
);
// A light edge around a dark key: it stands out on the menu's dark background.
if pad_glyph::art(atlas, b, style).is_none() {
batch.fill(
atlas,
x - 2.0,
y - 3.0,
kw + 2.0,
pad_glyph::HEIGHT + 2.0,
rgba(170, 170, 170, 255),
);
}
pad_glyph::draw(batch, atlas, b, style, x, y);
x += kw + 4.0;
}
batch.text(atlas, &words, x + 2.0, y, rgba(240, 240, 240, 255), true);
+15 -3
View File
@@ -836,7 +836,14 @@ impl<G: ChunkGenerator> Source for Jobs<G> {
Job::Mesh(..) => 5,
_ => 6,
};
(kind, crate::spans::thread_cpu().unwrap_or_default())
// The other jobs (saves, light views, freed meshes) are many and short: counted, not
// timed, so the clock reads don't cost more than they do.
let cpu = if kind == 6 {
std::time::Duration::ZERO
} else {
crate::spans::thread_cpu().unwrap_or_default()
};
(kind, cpu)
});
let _tag = horizon_threads::heap_tag(match &job.job {
Job::Generate(..) => "stream.generate",
@@ -865,8 +872,13 @@ impl<G: ChunkGenerator> Source for Jobs<G> {
} else {
kind
};
let cpu = crate::spans::thread_cpu().unwrap_or_default();
job_stats::add(kind, cpu.saturating_sub(started).as_nanos() as u64);
let nanos = if kind == 6 {
0
} else {
let cpu = crate::spans::thread_cpu().unwrap_or_default();
cpu.saturating_sub(started).as_nanos() as u64
};
job_stats::add(kind, nanos);
}
if let Some(done) = done {
self.shared.done.send(done);
+80 -2
View File
@@ -201,9 +201,14 @@ pub fn set_action_glyphs(on: bool) {
ACTION_GLYPHS.set(on);
}
/// How much of a declared button's left the glyph takes from its label.
/// How much of a declared button's left the glyph takes from its label: a face button's, the only
/// kind declared buttons show, and no family's is wider than its art.
pub fn action_glyph_room() -> f32 {
if ACTION_GLYPHS.get() { 14.0 } else { 0.0 }
if ACTION_GLYPHS.get() {
crate::pad_glyph::FACE_WIDTH + 3.0
} else {
0.0
}
}
/// An interactive rectangle (GUI pixels) a controller can focus and click.
@@ -244,6 +249,41 @@ pub fn clip_targets(mark: usize, y0: f32, y1: f32) {
});
}
/// How many labelled widgets and targets the frame has so far, for [`clip_frame`].
pub fn frame_mark() -> (usize, usize) {
let labelled = WIDGETS.with_borrow(|(current, _, _)| current.len());
(labelled, targets_mark())
}
/// Drop the labelled widgets and targets since `mark` whose middle isn't in `y0..y1` (GUI pixels):
/// what a scrolling body clipped away, where a press wouldn't land.
pub fn clip_frame((labelled, targets): (usize, usize), y0: f32, y1: f32) {
let shown = |y: f32, h: f32| (y0..y1).contains(&(y + h / 2.0));
WIDGETS.with_borrow_mut(|(current, _, _)| {
let mut at = 0;
current.retain(|w| {
at += 1;
at <= labelled || shown(w.y, w.h)
});
});
TARGETS.with_borrow_mut(|(current, _, _)| {
let mut at = 0;
current.retain(|t| {
at += 1;
at <= targets || shown(t.rect[1], t.rect[3])
});
});
}
/// A prompt comes up over what the frame drew so far: what it covers can't be reached, by the
/// pointer or a controller, so its widgets, targets, declared buttons and lists go.
pub fn cover_frame() {
WIDGETS.with_borrow_mut(|(current, _, _)| current.clear());
TARGETS.with_borrow_mut(|(current, _, _)| current.clear());
ACTIONS.with_borrow_mut(|(current, _)| current.clear());
SCROLL_AREAS.with_borrow_mut(|(current, _)| current.clear());
}
/// This frame drew a list with something to scroll, in `rect`, scrolled `amount`
/// of `max`: a focus step goes through its hidden rows before it leaves
/// (`gamepad::list_step`).
@@ -1349,4 +1389,42 @@ mod tests {
"the clipped row is gone, earlier ones stay"
);
}
#[test]
fn a_scrolled_body_loses_what_it_clips_and_nothing_before_it() {
let _ = take();
target([0.0, 0.0, 5.0, 5.0]);
record("warning", [0.0, 0.0, 5.0, 5.0], true);
let mark = frame_mark();
record("shown", [0.0, 10.0, 5.0, 10.0], true);
target([0.0, 10.0, 5.0, 10.0]);
// Half in view is still pressed at its middle, which a mouse could reach.
record("half", [0.0, 25.0, 5.0, 10.0], true);
target([0.0, 25.0, 5.0, 10.0]);
record("gone", [0.0, 40.0, 5.0, 10.0], true);
target([0.0, 40.0, 5.0, 10.0]);
clip_frame(mark, 8.0, 32.0);
end_frame(101);
let names: Vec<String> = last_labelled().into_iter().map(|w| w.label).collect();
assert_eq!(names, ["warning", "shown", "half"]);
assert_eq!(last_targets().len(), 3);
}
#[test]
fn a_prompt_covers_what_was_drawn_before_it() {
let _ = take();
record("under", [0.0, 0.0, 5.0, 5.0], true);
target([0.0, 10.0, 5.0, 5.0]);
declare_action(Action::Done, [0.0, 0.0, 5.0, 5.0], true);
scroll_area([0.0, 0.0, 5.0, 5.0], 0.0, 5.0);
cover_frame();
record("over", [0.0, 20.0, 5.0, 5.0], true);
set_action_screen(7);
end_frame(102);
let names: Vec<String> = last_labelled().into_iter().map(|w| w.label).collect();
assert_eq!(names, ["over"]);
assert!(last_targets().is_empty());
assert!(last_actions(7).is_empty());
assert!(last_scroll_areas().is_empty());
}
}
+13 -1
View File
@@ -254,7 +254,11 @@ fn normalize_end(data: &mut [u8]) {
}
let disk = u16_at(data, end + 4);
let with_directory = u16_at(data, end + 6);
if disk == with_directory && u16_at(data, end + 8) == count && u16_at(data, end + 10) == count {
if disk == 0
&& with_directory == 0
&& u16_at(data, end + 8) == count
&& u16_at(data, end + 10) == count
{
return;
}
data[end + 4..end + 10].fill(0);
@@ -311,6 +315,14 @@ mod tests {
assert_eq!(zip.read("a/b.txt").unwrap(), b"hello hello hello hello");
}
#[test]
fn a_file_is_opened_with_equal_nonzero_disk_numbers() {
let mut bytes = archive();
let end = bytes.len() - END_HEADER;
bytes[end + 4..end + 8].fill(0xFF);
let zip = opened("end-disks", &bytes).unwrap();
assert_eq!(zip.read("a/b.txt").unwrap(), b"hello hello hello hello");
}
#[test]
fn a_file_that_is_no_archive_does_not_open() {
assert!(opened("empty", b"").is_err());
assert!(opened("garbage", b"PK not really").is_err());
+4 -4
View File
@@ -54,6 +54,8 @@ sha2 = "0.10.9"
sha1 = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1.0.151", features = ["raw_value"] }
# server.toml's [combat], which the dedicated server reads itself.
toml = "1.1"
# hlsl-out: the gfx.rs tests translate every shader as wgpu's DirectX 12 backend does. A normal feature,
# not a dev-dependency's, so builds, tests and examples share one build of naga, wgpu and this crate.
naga = { version = "30.0.1", features = ["wgsl-in", "hlsl-out"] }
@@ -104,10 +106,6 @@ objc2-foundation = { version = "0.3", default-features = false, features = ["std
# DirectX 12 driver (`graphicsApi`, graphics.rs).
wgpu = { version = "30.0.1", default-features = false, features = ["gles"] }
[target.'cfg(windows)'.build-dependencies]
# The icon resource in the .exe (build.rs).
winresource = "0.1"
[features]
default = ["audio", "controllers"]
# Sound output. Without it (and `controllers`) the program links neither libasound nor libudev on
@@ -150,6 +148,8 @@ path = "examples/native_gui_overlay_probe.rs"
[build-dependencies]
horizon-mod-api-gen = { path = "../horizon-mod-api-gen" }
# The build script runs on the host, but embeds resources based on the target OS.
winresource = "0.1"
[lints]
workspace = true
@@ -326,7 +326,6 @@
"horizon.osk.close": "Maak toe",
"horizon.extras.chat_animation.tooltip": "'n Nuwe kletslyn gly van links af terwyl dit vervaag, in plaas daarvan om heel te lyk.",
"horizon.extras.chat_indicator.tooltip": "Die gekleurde balk aan die linkerkant van 'n kletslyn.",
"horizon.extras.wheel_cycles_settings.tooltip": "Die rolwiel oor 'n knoppie in die instellings verander die waarde daarvan, soos in vanielje. Af, die wiel blaai net deur die lys.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs kry 'n Voeg by-knoppie wat Modrinth deursoek en 'n pakket in jou pakke-lêergids installeer. Af, die speletjie kontak nooit Modrinth nie.",
"horizon.extras.splitscreen.tooltip": "In 'n wêreld kan 'n ander beheerder as 'n nuwe speler aansluit: druk A wanneer dit vra, kies dan 'n profiel en waar om te speel.",
"horizon.splitscreen.setup.monitor_unknown": "'n monitor",
@@ -472,8 +471,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Versteek: elke Horizon-speler sien jou sonder jou skoonheidsmiddels, jy ingesluit. Dieselfde skakelaar as die lanseerder se klerekas.",
"horizon.extras.cosmetics_others": "Ander spelers se skoonheidsmiddels",
"horizon.extras.cosmetics_others.tooltip": "Wat ander Horizon-spelers dra: alles, net hul naamplaatjiestyle, of niks. Versteek, die speletjie vra nooit wat hulle dra nie.",
"horizon.extras.cosmetics_auras": "Ander se aura's en roetes",
"horizon.extras.cosmetics_auras.tooltip": "Auras en roetes wat ander spelers dra. Af by verstek.",
"horizon.extras.cosmetics_hide_fx": "Versteek ander spelers se spore en auras",
"horizon.extras.cosmetics_hide_fx.tooltip": "Ander spelers se spore en auras bly versteek; hul ander kosmetiese items wys steeds. By verstek af.",
"horizon.extras.cosmetics.shown": "Getoon",
"horizon.extras.cosmetics.hidden": "Versteek",
"horizon.extras.cosmetics.name_tags": "Slegs naamplaatjies"
@@ -341,7 +341,6 @@
"horizon.osk.close": "إغلاق",
"horizon.extras.chat_animation.tooltip": "ينزلق خط الدردشة الجديد من اليسار بينما يتلاشى للداخل، بدلاً من أن يظهر كاملاً.",
"horizon.extras.chat_indicator.tooltip": "الشريط الملون الموجود على يسار خط الدردشة.",
"horizon.extras.wheel_cycles_settings.tooltip": "عجلة التمرير فوق زر في الإعدادات تغير قيمته، كما هو الحال في الفانيليا. مع إيقاف التشغيل، تقوم العجلة بتمرير القائمة فقط.",
"horizon.extras.pack_browser.tooltip": "تحديد حزم الموارد يحصل على زر \"إضافة\" الذي يبحث في Modrinth ويقوم بتثبيت حزمة في مجلد الحزم الخاص بك. مع إيقاف التشغيل، لن تتصل اللعبة أبدًا بـ Modrinth.",
"horizon.extras.splitscreen.tooltip": "في عالم ما، يمكن لوحدة تحكم أخرى الانضمام كلاعب جديد: اضغط على A عندما يُطلب منك ذلك، ثم اختر ملفًا شخصيًا ومكان اللعب.",
"horizon.splitscreen.setup.monitor": "مراقب %s",
@@ -491,8 +490,8 @@
"horizon.extras.cosmetics_mine.tooltip": "مخفي: يراك كل لاعب في Horizon بدون مستحضرات التجميل الخاصة بك، بما في ذلك أنت. نفس مفتاح خزانة المشغل.",
"horizon.extras.cosmetics_others": "مستحضرات التجميل للاعبين الآخرين",
"horizon.extras.cosmetics_others.tooltip": "ما يرتديه لاعبو Horizon الآخرون: كل شيء، فقط أنماط بطاقة الاسم الخاصة بهم، أو لا شيء. مخفيًا، اللعبة لا تسأل أبدًا عما يرتدونه.",
"horizon.extras.cosmetics_auras": "هالات ومسارات الآخرين",
"horizon.extras.cosmetics_auras.tooltip": "الهالات والمسارات التي يرتديها اللاعبون الآخرون. إيقاف بشكل افتراضي.",
"horizon.extras.cosmetics_hide_fx": "إخفاء آثار وهالات اللاعبين الآخرين",
"horizon.extras.cosmetics_hide_fx.tooltip": "تبقى آثار وهالات اللاعبين الآخرين مخفية؛ وتظل مستحضراتهم الأخرى ظاهرة. معطّل افتراضيًا.",
"horizon.extras.cosmetics.shown": "معروض",
"horizon.extras.cosmetics.hidden": "مخفي",
"horizon.extras.cosmetics.name_tags": "علامات الاسم فقط"
@@ -317,7 +317,6 @@
"horizon.osk.close": "Bağlayın",
"horizon.extras.chat_animation.tooltip": "Yeni söhbət xətti bütöv görünmək əvəzinə soldan içəri sürüşür.",
"horizon.extras.chat_indicator.tooltip": "Söhbət xəttinin solunda olan rəngli bar.",
"horizon.extras.wheel_cycles_settings.tooltip": "Parametrlərdəki düymənin üzərindəki sürüşdürmə çarxı vanildə olduğu kimi dəyərini dəyişir. Söndürülmüş, təkər yalnız siyahını sürüşdürür.",
"horizon.extras.pack_browser.tooltip": "Resurs Paketlərini seçin Modrinth-də axtarış aparan və paketlər qovluğuna paket quraşdıran Əlavə et düyməsini alır. Off, oyun Modrinthlə heç vaxt əlaqə saxlamır.",
"horizon.extras.splitscreen.tooltip": "Dünyada başqa bir nəzarətçi yeni oyunçu kimi qoşula bilər: soruşduqda A düyməsini basın, sonra profil və harada oynayacağınızı seçin.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
@@ -463,8 +462,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Gizli: hər Horizon oyunçu sizi kosmetikanız olmadan görür. Başlatma şkafı ilə eyni açar.",
"horizon.extras.cosmetics_others": "Digər oyunçuların kosmetikası",
"horizon.extras.cosmetics_others.tooltip": "Digər Horizon oyunçular nə geyinirlər: hər şey, yalnız ad etiketi üslubları və ya heç nə. Gizli, oyun heç vaxt nə geyindiklərini soruşmur.",
"horizon.extras.cosmetics_auras": "Başqalarının auraları və izləri",
"horizon.extras.cosmetics_auras.tooltip": "Digər oyunçuların geydiyi auralar və izlər. Defolt olaraq söndürülür.",
"horizon.extras.cosmetics_hide_fx": "Digər oyunçuların izlərini və auralarını gizlət",
"horizon.extras.cosmetics_hide_fx.tooltip": "Digər oyunçuların izləri və auraları gizli qalır; digər kosmetikaları görünməyə davam edir. Defolt olaraq söndürülüb.",
"horizon.extras.cosmetics.shown": "Göstərildi",
"horizon.extras.cosmetics.hidden": "Gizli",
"horizon.extras.cosmetics.name_tags": "Yalnız ad etiketləri"
@@ -333,7 +333,6 @@
"horizon.osk.close": "Ябыҡ",
"horizon.extras.chat_animation.tooltip": "Яңы чат һыҙығы һулдан шыуып инә, ә ул һүнә, урынына бөтөн күренә.",
"horizon.extras.chat_indicator.tooltip": "Чат һыҙығының һул яғында төҫлө һыҙыҡ.",
"horizon.extras.wheel_cycles_settings.tooltip": "Көйләүҙәрҙәге төймә өҫтөндәге прокрутка тәгәрмәсе үҙенең ҡиммәтен үҙгәртә, ванильдәге кеүек. Һүндерелгән, тәгәрмәс исемлекте генә йөрөтә.",
"horizon.extras.pack_browser.tooltip": "Һайлау ресурс пакеттары ала өҫтәү төймәһе, эҙләү Modrinth һәм һеҙҙең пакеттар папкаһына пакет ҡуя. Һүндерелгән, уйын бер ҡасан да бәйләнешкә инә Modrinth.",
"horizon.extras.splitscreen.tooltip": "Донъяла, икенсе контроллер яңы уйынсы булараҡ ҡушыла ала: баҫығыҙ А, ҡасан ул һорай, һуңынан профиль һайлап һәм ҡайҙа уйнау.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -489,8 +488,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Йәшерен: һәр Horizon уйынсы һеҙҙең косметикаһыҙ күрә, һеҙ ҙә индерелгән. Шул уҡ переключатель стартер&#8217;s Гардероб.",
"horizon.extras.cosmetics_others": "Башҡа уйынсыларҙың косметикаһы",
"horizon.extras.cosmetics_others.tooltip": "Башҡа ниндәй Horizon уйынсылар кейә: барыһы ла, тик уларҙы исем биркаһы стилдәре, йәки бер нәмә лә. Йәшерен, уйын бер ҡасан да һорамай, улар нимә кейә.",
"horizon.extras.cosmetics_auras": "Башҡаларҙың ауралары һәм эҙҙәре",
"horizon.extras.cosmetics_auras.tooltip": "Аура һәм эҙҙәр башҡа уйынсылар кейә. Һүндерелгән по умолчанию.",
"horizon.extras.cosmetics_hide_fx": "Башҡа уйынсыларҙың эҙҙәрен һәм ауралары йәшереү",
"horizon.extras.cosmetics_hide_fx.tooltip": "Башҡа уйынсыларҙың эҙҙәре һәм ауралары йәшерен ҡала; уларҙың башҡа косметикаһы күренә. Ғәҙәттә һүндерелгән.",
"horizon.extras.cosmetics.shown": "Күрһәтелгән",
"horizon.extras.cosmetics.hidden": "Йәшерен",
"horizon.extras.cosmetics.name_tags": "Исем билдәләре генә"
@@ -326,7 +326,6 @@
"horizon.osk.close": "Блізка",
"horizon.extras.chat_animation.tooltip": "Новы радок чата слізгае злева, пакуль ён знікае, замест таго, каб выглядаць цалкам.",
"horizon.extras.chat_indicator.tooltip": "Каляровая паласа злева ад радка чата.",
"horizon.extras.wheel_cycles_settings.tooltip": "Кола пракруткі над кнопкай у наладах змяняе яе значэнне, як у vanilla. Выключана, кола толькі пракручвае спіс.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs атрымлівае кнопку «Дадаць», якая выконвае пошук у Modrinth і ўсталёўвае пакет у вашу тэчку пакетаў. Выключана, гульня ніколі не звязваецца з Modrinth.",
"horizon.extras.splitscreen.tooltip": "У свеце іншы кантролер можа далучыцца як новы гулец: націсніце A, калі ён запытае, затым выберыце профіль і дзе гуляць.",
"horizon.splitscreen.setup.monitor": "Манітор %s",
@@ -475,8 +474,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Схаваны: кожны гулец Horizon бачыць вас без вашай касметыкі, у тым ліку і вас. The same switch as the launcher's Wardrobe.",
"horizon.extras.cosmetics_others": "Касметыка іншых гульцоў",
"horizon.extras.cosmetics_others.tooltip": "Што носяць іншыя гульцы Horizon: усё, толькі стылі тэгаў з імёнамі або нічога. Hidden, the game never asks what they wear.",
"horizon.extras.cosmetics_auras": "Чужыя аўры і шлейфы",
"horizon.extras.cosmetics_auras.tooltip": "Auras and trails other players wear. Выключана па змаўчанні.",
"horizon.extras.cosmetics_hide_fx": "Схаваць сляды і аўры іншых гульцоў",
"horizon.extras.cosmetics_hide_fx.tooltip": "Сляды і аўры іншых гульцоў схаваныя; іх астатняя касметыка бачная. Па змаўчанні выкл.",
"horizon.extras.cosmetics.shown": "Паказана",
"horizon.extras.cosmetics.hidden": "Схаваны",
"horizon.extras.cosmetics.name_tags": "Толькі біркі з імёнамі"
@@ -324,7 +324,6 @@
"horizon.osk.close": "затвори",
"horizon.extras.chat_animation.tooltip": "Нова линия за чат се плъзга отляво, докато избледнява, вместо да се показва цяла.",
"horizon.extras.chat_indicator.tooltip": "Цветната лента отляво на линия за чат.",
"horizon.extras.wheel_cycles_settings.tooltip": "Колелцето за превъртане над бутон в настройките променя стойността му, както при vanilla. Изключено, колелото само превърта списъка.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs получава бутон за добавяне, който търси в Modrinth и инсталира пакет във вашата папка с пакети. Off, играта никога не се свързва с Modrinth.",
"horizon.extras.splitscreen.tooltip": "В един свят друг контролер може да се присъедини като нов играч: натиснете A, когато поиска, след това изберете профил и къде да играете.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -470,8 +469,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Скрит: всеки играч на Horizon ви вижда без вашата козметика, включително и вие. Същият превключвател като гардероба на стартера.",
"horizon.extras.cosmetics_others": "Козметика за други играчи",
"horizon.extras.cosmetics_others.tooltip": "Какво носят другите играчи на Horizon: всичко, само техните стилове на имената или нищо. Скрито, играта никога не пита какво носят.",
"horizon.extras.cosmetics_auras": "Аури и следи на други",
"horizon.extras.cosmetics_auras.tooltip": "Аури и пътеки, които другите играчи носят. Изключено по подразбиране.",
"horizon.extras.cosmetics_hide_fx": "Скриване на следите и аурите на другите играчи",
"horizon.extras.cosmetics_hide_fx.tooltip": "Следите и аурите на другите играчи остават скрити; другите им козметики се виждат. Изключено по подразбиране.",
"horizon.extras.cosmetics.shown": "Показано",
"horizon.extras.cosmetics.hidden": "Скрити",
"horizon.extras.cosmetics.name_tags": "Само етикети с имена",
@@ -325,7 +325,6 @@
"horizon.osk.close": "Serriñ",
"horizon.extras.chat_animation.tooltip": "Ur linenn flapañ nevez a zeu eus an tu kleiz e-keit ma vez o vont da get, e-lec'h dont war wel a-bezh.",
"horizon.extras.chat_indicator.tooltip": "Ar varrenn livet a-gleiz d'ul linenn flapañ.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ar rod riklañ war ur bouton en arventennoù a gemm e dalvoudegezh, evel e vanilha. Diweredekaet, ne ra ar rod nemet riklañ ar roll.",
"horizon.extras.pack_browser.tooltip": "Dibab pakadoù arc'hwelioù a resev ur bouton Ouzhpennañ a glask Modrinth hag a staliañ ur pakad en ho teuliad pakadoù. Diweredekaet, ne vez ket darempredet ar c'hoari gant Modrinth.",
"horizon.extras.splitscreen.tooltip": "En ur bed e c'hall ur c'hontroller all mont e-barzh evel c'hoarier nevez : pouezit war A pa c'houlenn, ha dibabit ur profil ha pelec'h c'hoari.",
"horizon.splitscreen.setup.monitor": "Heuliañ %s",
@@ -474,8 +473,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Kuzhet : pep c'hoarier Horizon a wel ac'hanoc'h hep ho kosmetikoù, c'hwi en o zouez. Ar memes treuzfurmadur hag armel al lañser.",
"horizon.extras.cosmetics_others": "Kosmetikoù ar c'hoarierien all",
"horizon.extras.cosmetics_others.tooltip": "Ar pezh a vez gwisket gant ar c'hoarierien Horizon all : pep tra, o doareoù tikedenn anv hepken, pe netra. Kuzhet, ar c'hoari ne c'houlenn ket biskoazh petra a wiskont.",
"horizon.extras.cosmetics_auras": "Auraoù ha roudoù tud all",
"horizon.extras.cosmetics_auras.tooltip": "Auraoù ha roudoù a vez gwisket gant c'hoarierien all. Diweredekaet dre ziouer.",
"horizon.extras.cosmetics_hide_fx": "Kuzhat roudoù hag aouraoù ar c'hoarierien all",
"horizon.extras.cosmetics_hide_fx.tooltip": "Kuzhet e chom roudoù hag aouraoù ar c'hoarierien all; diskouezet e vez o c'hinkladurioù all. Diweredekaet dre ziouer.",
"horizon.extras.cosmetics.shown": "Diskouezet",
"horizon.extras.cosmetics.hidden": "Kuzhet",
"horizon.extras.cosmetics.name_tags": "Tikedennoù anvioù hepken"
@@ -281,7 +281,6 @@
"horizon.osk.close": "Zatvori",
"horizon.extras.chat_animation.tooltip": "Nova linija ćaskanja klizi s lijeve strane dok se gasi, umjesto da izgleda kao cijela.",
"horizon.extras.chat_indicator.tooltip": "Traka u boji lijevo od linije za ćaskanje.",
"horizon.extras.wheel_cycles_settings.tooltip": "Točkić za pomeranje preko dugmeta u podešavanjima menja svoju vrednost, kao u vaniliji. Isključeno, kotačić samo pomiče listu.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs dobija dugme Dodaj koje pretražuje Modrinth i instalira paket u fasciklu sa paketima. Isključeno, igra nikada ne kontaktira Modrinth.",
"horizon.extras.splitscreen.tooltip": "U svijetu se drugi kontrolor može pridružiti kao novi igrač: pritisnite A kada to zatraži, a zatim odaberite profil i gdje ćete igrati.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
@@ -415,8 +414,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Skriveno: svaki Horizon igrač vas vidi bez vaše kozmetike, uključujući i vas. Isti prekidač kao Ormar lansera.",
"horizon.extras.cosmetics_others": "Kozmetika drugih igrača",
"horizon.extras.cosmetics_others.tooltip": "Šta drugi Horizon igrači nose: sve, samo stilove oznaka sa imenom ili ništa. Skriveno, igra nikad ne pita šta nose.",
"horizon.extras.cosmetics_auras": "Aure i tragovi drugih",
"horizon.extras.cosmetics_auras.tooltip": "Aure i staze koje drugi igrači nose. Podrazumevano isključeno.",
"horizon.extras.cosmetics_hide_fx": "Sakrij tragove i aure drugih igrača",
"horizon.extras.cosmetics_hide_fx.tooltip": "Tragovi i aure drugih igrača ostaju skriveni; njihova ostala kozmetika se i dalje vidi. Isključeno po zadanom.",
"horizon.extras.cosmetics.shown": "Prikazano",
"horizon.extras.cosmetics.hidden": "Skriveno",
"horizon.extras.cosmetics.name_tags": "Samo oznake sa imenom",
@@ -319,7 +319,6 @@
"horizon.osk.close": "Tancar",
"horizon.extras.chat_animation.tooltip": "Una nova línia de xat es desplaça des de l'esquerra mentre s'esvaeix, en lloc de semblar sencera.",
"horizon.extras.chat_indicator.tooltip": "La barra de colors a l'esquerra d'una línia de xat.",
"horizon.extras.wheel_cycles_settings.tooltip": "La roda de desplaçament sobre un botó a la configuració canvia el seu valor, com a la vainilla. Desactivada, la roda només desplaça la llista.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs obté un botó Afegeix que cerca Modrinth i instal·la un paquet a la carpeta de paquets. Desactivat, el joc mai contacta amb Modrinth.",
"horizon.extras.splitscreen.tooltip": "En un món, un altre controlador pot unir-se com a jugador nou: premeu A quan ho demani, després trieu un perfil i on jugar.",
"horizon.splitscreen.setup.monitor_unknown": "un monitor",
@@ -463,8 +462,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Ocult: tots els jugadors d'Horizon et veuen sense els teus cosmètics, tu inclòs. El mateix interruptor que l'armari del llançador.",
"horizon.extras.cosmetics_others": "Cosmètica d'altres jugadors",
"horizon.extras.cosmetics_others.tooltip": "Què porten altres jugadors d'Horizon: tot, només els seus estils d'etiquetes de nom o res. Amagat, el joc mai pregunta què porten.",
"horizon.extras.cosmetics_auras": "Les aures i els camins dels altres",
"horizon.extras.cosmetics_auras.tooltip": "Aures i rastres que porten altres jugadors. Desactivat per defecte.",
"horizon.extras.cosmetics_hide_fx": "Amaga les esteles i les aures dels altres jugadors",
"horizon.extras.cosmetics_hide_fx.tooltip": "Les esteles i les aures dels altres jugadors queden amagades; els seus altres cosmètics es continuen veient. Desactivat per defecte.",
"horizon.extras.cosmetics.shown": "Es mostra",
"horizon.extras.cosmetics.hidden": "Ocult",
"horizon.extras.cosmetics.name_tags": "Només etiquetes de nom",
@@ -311,7 +311,6 @@
"horizon.osk.close": "Zavřít",
"horizon.extras.chat_animation.tooltip": "Nová čára chatu se vsune zleva, zatímco se mizí, místo aby vypadala celá.",
"horizon.extras.chat_indicator.tooltip": "Barevný pruh nalevo od chatovací linky.",
"horizon.extras.wheel_cycles_settings.tooltip": "Rolovací kolečko nad tlačítkem v nastavení mění jeho hodnotu, jako u vanilky. Vypnuto, kolečko pouze posouvá seznam.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs dostane tlačítko Přidat, které prohledá Modrinth a nainstaluje balíček do vaší složky balíčků. Vypnuto, hra nikdy nekontaktuje Modrinth.",
"horizon.extras.splitscreen.tooltip": "Ve světě se může jako nový hráč připojit další ovladač: stiskněte A, když se zeptá, pak vyberte profil a kde hrát.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
@@ -453,8 +452,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Skryté: každý hráč Horizon vás vidí bez vaší kosmetiky, včetně vás. Stejný vypínač jako skříň odpalovacího zařízení.",
"horizon.extras.cosmetics_others": "Kosmetika ostatních hráčů",
"horizon.extras.cosmetics_others.tooltip": "Co nosí ostatní hráči Horizon: všechno, jen styly jmenovek nebo nic. Skryto, hra se nikdy neptá, co nosí.",
"horizon.extras.cosmetics_auras": "Aury a stezky ostatních",
"horizon.extras.cosmetics_auras.tooltip": "Aury a stezky, které nosí ostatní hráči. Ve výchozím nastavení vypnuto.",
"horizon.extras.cosmetics_hide_fx": "Skrýt stopy a aury ostatních hráčů",
"horizon.extras.cosmetics_hide_fx.tooltip": "Stopy a aury ostatních hráčů zůstanou skryté; jejich ostatní kosmetika je dál vidět. Ve výchozím stavu vypnuto.",
"horizon.extras.cosmetics.shown": "Zobrazeno",
"horizon.extras.cosmetics.hidden": "Skrytá",
"horizon.extras.cosmetics.name_tags": "Pouze jmenovky"
@@ -330,7 +330,6 @@
"horizon.osk.close": "Хуп",
"horizon.extras.chat_animation.tooltip": "Ҫӗнӗ чат йӗрки сулахай енчен шуса кӗрет, вӑл вара пӗтӗмӗшпех курӑнмасть.",
"horizon.extras.chat_indicator.tooltip": "Чат йӗркин сулахай енчи тӗслӗ йӗр.",
"horizon.extras.wheel_cycles_settings.tooltip": "Настройкӑсенчи кнопка урлӑ ҫаврӑнакан ҫаврашка унӑн хакне улӑштарать, ваниль ҫинчи пекех. Сӳнтерсен, урапа списока ҫеҫ куҫарать.",
"horizon.extras.pack_browser.tooltip": "Ресурс пакетӗсене суйласа илсен, хушма кнопка пулать, вӑл Modrinth шырать те сирӗн пакетсен папкине пакет вырнаҫтарать. Ҫук, вӑйӑ нихҫан та Modrinth ҫыхӑнмасть.",
"horizon.extras.splitscreen.tooltip": "Тӗнчере тепӗр контроллер ҫӗнӗ вылякан пулса хутшӑнма пултарать: вӑл ыйтсан А пусмалла, унтан профиль суйласа илмелле тата ӑҫта вылямаллине суйласа илмелле.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -485,8 +484,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Пытарнӑ: кашни Horizon вылякан сире косметикӑсӑр курать, эсир те ҫав шутра. Пуҫкаҫӑн гардеробӗнчи пекех переключатель.",
"horizon.extras.cosmetics_others": "Ытти вылякансен косметики",
"horizon.extras.cosmetics_others.tooltip": "Ытти Horizon вылякансем мӗн тӑхӑнаҫҫӗ: пурте, вӗсен ячӗсен стилӗсем ҫеҫ, е нимӗн те мар. Вӑрттӑн, вӑйӑ нихҫан та вӗсем мӗн тӑхӑннине ыйтмасть.",
"horizon.extras.cosmetics_auras": "Ыттисен аурисем тата йӗрӗсем",
"horizon.extras.cosmetics_auras.tooltip": "Ытти вылякансем тӑхӑнакан аурӑсемпе йӗрсем. Выключен по умолчанию.",
"horizon.extras.cosmetics_hide_fx": "Ытти вӑййисен йӗрӗсене тата аурисене пытар",
"horizon.extras.cosmetics_hide_fx.tooltip": "Ытти вӑййисен йӗрӗсем тата аурисем пытарӑнса юлаҫҫӗ; вӗсен ытти косметики курӑнать. Йӑлана кура сӳнтернӗ.",
"horizon.extras.cosmetics.shown": "Кӑтартнӑ",
"horizon.extras.cosmetics.hidden": "Пытарнӑ",
"horizon.extras.cosmetics.name_tags": "Ят паллисем кӑна"
@@ -317,7 +317,6 @@
"horizon.osk.close": "Cau",
"horizon.extras.chat_animation.tooltip": "Mae llinell sgwrsio newydd yn llithro i mewn o'r chwith tra mae'n pylu i mewn, yn lle ymddangos yn gyfan.",
"horizon.extras.chat_indicator.tooltip": "Y bar lliw ar ochr chwith llinell sgwrsio.",
"horizon.extras.wheel_cycles_settings.tooltip": "Mae'r olwyn sgrolio dros fotwm yn y gosodiadau yn newid ei werth, fel yn y fanila. I ffwrdd, mae'r olwyn yn sgrolio'r rhestr yn unig.",
"horizon.extras.pack_browser.tooltip": "Mae Dewis Pecynnau Adnoddau yn cael botwm Ychwanegu sy'n chwilio Modrinth ac yn gosod pecyn yn eich ffolder pecynnau. I ffwrdd, nid yw'r gêm byth yn cysylltu â Modrinth.",
"horizon.extras.splitscreen.tooltip": "Mewn byd, gall rheolydd arall ymuno fel chwaraewr newydd: pwyswch A pan fydd yn gofyn, yna dewiswch broffil a ble i chwarae.",
"horizon.splitscreen.setup.monitor": "Monitro %s",
@@ -460,8 +459,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Cudd: mae pob chwaraewr Horizon yn eich gweld heb eich colur, fe wnaethoch chi gynnwys. Yr un switsh â Wardrob y lansiwr.",
"horizon.extras.cosmetics_others": "Colur chwaraewyr eraill",
"horizon.extras.cosmetics_others.tooltip": "Beth mae chwaraewyr Horizon eraill yn ei wisgo: popeth, dim ond eu steil tag enw, neu ddim byd. Yn gudd, nid yw'r gêm byth yn gofyn beth maen nhw'n ei wisgo.",
"horizon.extras.cosmetics_auras": "Auras a llwybrau eraill",
"horizon.extras.cosmetics_auras.tooltip": "Auras a llwybrau mae chwaraewyr eraill yn eu gwisgo. Wedi'i ddiffodd yn ddiofyn.",
"horizon.extras.cosmetics_hide_fx": "Cuddio llwybrau ac awrâu chwaraewyr eraill",
"horizon.extras.cosmetics_hide_fx.tooltip": "Mae llwybrau ac awrâu chwaraewyr eraill yn aros yn gudd; mae eu colur arall yn dal i ddangos. I ffwrdd yn ddiofyn.",
"horizon.extras.cosmetics.shown": "Dangosir",
"horizon.extras.cosmetics.hidden": "Cudd",
"horizon.extras.cosmetics.name_tags": "Tagiau enw yn unig"
@@ -301,7 +301,6 @@
"horizon.osk.close": "Luk",
"horizon.extras.chat_animation.tooltip": "En ny chatlinje glider ind fra venstre, mens den toner ind, i stedet for at virke hel.",
"horizon.extras.chat_indicator.tooltip": "Den farvede bjælke til venstre for en chatlinje.",
"horizon.extras.wheel_cycles_settings.tooltip": "Scrollhjulet over en knap i indstillingerne ændrer dens værdi, som i vanilje. Fra, hjulet ruller kun listen.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs får en Tilføj knap, der søger mod Modrinth og installerer en pakke i din pakkemappe. Off, spillet kontakter aldrig Modrinth.",
"horizon.extras.splitscreen.tooltip": "I en verden kan en anden controller deltage som en ny spiller: Tryk på A, når den spørger, vælg derefter en profil og hvor du vil spille.",
"horizon.splitscreen.setup.monitor": "Overvåg %s",
@@ -440,8 +439,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Skjult: alle Horizon-spillere ser dig uden din kosmetik, inklusive dig. Den samme kontakt som løfterakettens garderobe.",
"horizon.extras.cosmetics_others": "Andre spilleres kosmetik",
"horizon.extras.cosmetics_others.tooltip": "Hvad andre Horizon-spillere bærer: alt, kun deres navnemærke eller intet. Skjult spørger spillet aldrig, hvad de har på.",
"horizon.extras.cosmetics_auras": "Andres auraer og spor",
"horizon.extras.cosmetics_auras.tooltip": "Auraer og spor, som andre spillere bærer. Fra som standard.",
"horizon.extras.cosmetics_hide_fx": "Skjul andre spilleres spor og auraer",
"horizon.extras.cosmetics_hide_fx.tooltip": "Andre spilleres spor og auraer forbliver skjulte; deres andre kosmetikker vises stadig. Slået fra som standard.",
"horizon.extras.cosmetics.shown": "Vist",
"horizon.extras.cosmetics.hidden": "Skjult",
"horizon.extras.cosmetics.name_tags": "Kun navneskilte",
@@ -363,8 +363,8 @@
"horizon.emote_wheel.help": "Loslassen oder klicken zum Abspielen · 1–8 · Esc schließt",
"horizon.extras.cosmetics_others": "Kosmetik anderer Spieler",
"horizon.extras.cosmetics_others.tooltip": "Was andere Horizon-Spieler tragen: alles, nur ihre Namensschild-Stile oder nichts. Ausgeblendet fragt das Spiel nie, was sie tragen.",
"horizon.extras.cosmetics_auras": "Auren und Spuren anderer",
"horizon.extras.cosmetics_auras.tooltip": "Auren und Spuren, die andere Spieler tragen. Standardmäßig aus.",
"horizon.extras.cosmetics_hide_fx": "Spuren und Auren anderer Spieler ausblenden",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spuren und Auren anderer Spieler bleiben ausgeblendet; ihre anderen Kosmetika sind weiter sichtbar. Standardmäßig aus.",
"horizon.extras.cosmetics.shown": "Angezeigt",
"horizon.extras.cosmetics.hidden": "Ausgeblendet",
"horizon.extras.cosmetics.name_tags": "Nur Namensschilder",
@@ -317,7 +317,6 @@
"horizon.osk.close": "Κλείσιμο",
"horizon.extras.chat_animation.tooltip": "Μια νέα γραμμή συνομιλίας ολισθαίνει από τα αριστερά ενώ σβήνει, αντί να εμφανίζεται ολόκληρη.",
"horizon.extras.chat_indicator.tooltip": "Η έγχρωμη γραμμή στα αριστερά μιας γραμμής συνομιλίας.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ο τροχός κύλισης πάνω από ένα κουμπί στις ρυθμίσεις αλλάζει την τιμή του, όπως στη βανίλια. Ανενεργό, ο τροχός κάνει κύλιση μόνο στη λίστα.",
"horizon.extras.pack_browser.tooltip": "Το Select Resource Packs παίρνει ένα κουμπί Προσθήκη που αναζητά το Modrinth και εγκαθιστά ένα πακέτο στο φάκελο των πακέτων σας. Εκτός, το παιχνίδι δεν έρχεται ποτέ σε επαφή με τη Modrinth.",
"horizon.extras.splitscreen.tooltip": "Σε έναν κόσμο, ένας άλλος ελεγκτής μπορεί να εγγραφεί ως νέος παίκτης: πατήστε A όταν σας ζητήσει, μετά επιλέξτε ένα προφίλ και πού θα παίξετε.",
"horizon.splitscreen.setup.monitor": "Οθόνη %s",
@@ -462,8 +461,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Κρυφό: κάθε παίκτης του Horizon σας βλέπει χωρίς τα καλλυντικά σας, συμπεριλαμβανομένων εσείς. Ο ίδιος διακόπτης με την ντουλάπα του εκτοξευτή.",
"horizon.extras.cosmetics_others": "Καλλυντικά άλλων παικτών",
"horizon.extras.cosmetics_others.tooltip": "Τι φοράνε οι άλλοι παίκτες του Horizon: τα πάντα, μόνο τα στυλ των ετικετών τους ή τίποτα. Κρυφά, το παιχνίδι δεν ρωτά ποτέ τι φοράνε.",
"horizon.extras.cosmetics_auras": "Οι αύρες και τα μονοπάτια των άλλων",
"horizon.extras.cosmetics_auras.tooltip": "Αύρες και μονοπάτια φορούν άλλοι παίκτες. Απενεργοποίηση από προεπιλογή.",
"horizon.extras.cosmetics_hide_fx": "Απόκρυψη ιχνών και αυρών άλλων παικτών",
"horizon.extras.cosmetics_hide_fx.tooltip": "Τα ίχνη και οι αύρες των άλλων παικτών μένουν κρυφά· τα άλλα διακοσμητικά τους εξακολουθούν να φαίνονται. Ανενεργό από προεπιλογή.",
"horizon.extras.cosmetics.shown": "Εμφανίζεται",
"horizon.extras.cosmetics.hidden": "Κρυφό",
"horizon.extras.cosmetics.name_tags": "Μόνο ετικέτες ονομάτων",
@@ -13,6 +13,7 @@
"horizon.options.sneak_button": "Sneak Button",
"horizon.options.move_deadzone": "Move Deadzone",
"horizon.options.look_deadzone": "Look Deadzone",
"horizon.options.trigger_threshold": "Trigger Threshold",
"horizon.options.button_glyphs": "Button Glyphs",
"horizon.options.on_screen_keyboard": "On-Screen Keyboard",
"horizon.options.chat_delay": "Chat Delay",
@@ -80,7 +81,7 @@
"horizon.pages.open.mods": "Mods...",
"horizon.pages.open.agent": "AI Agent...",
"horizon.pages.open.horizon_graphics": "Horizon Graphics...",
"horizon.pages.open.controller_binds": "Controller Binds...",
"horizon.pages.open.controller_binds": "Bindings...",
"horizon.pages.open.horizon_settings": "Horizon Settings...",
"horizon.pages.open.studio": "Replay Studio...",
"horizon.studio.saved_recordings": "Saved Recordings",
@@ -97,6 +98,28 @@
"horizon.mods.settings": "Settings",
"horizon.mods.in_world": " (leave the world to change)",
"horizon.mods.open_folder": "Open Mod Folder",
"horizon.mods.browse": "Browse Mods...",
"horizon.mods.browse.title": "Browse Mods",
"horizon.mods.browse.search": "Search mods...",
"horizon.mods.browse.project": "Mod",
"horizon.mods.browse.loading": "Reading Horizon's mod registry...",
"horizon.mods.browse.failed": "Couldn't read the mod registry: %s",
"horizon.mods.browse.none": "No mod found",
"horizon.mods.browse.has_update": " (update)",
"horizon.mods.browse.is_installed": " (installed)",
"horizon.mods.browse.by": "by %s",
"horizon.mods.browse.installing": "Installing...",
"horizon.mods.browse.install": "Install",
"horizon.mods.browse.update": "Update",
"horizon.mods.browse.installed": "Installed",
"horizon.mods.browse.open_page": "Open Page",
"horizon.mods.browse.enabled": "Mod",
"horizon.mods.browse.changelog": "Changelog",
"horizon.mods.browse.done": "%s %s is installed and on.",
"horizon.mods.browse.install_failed": "Couldn't install it: %s",
"horizon.mods.switch.worldgen": "changes world generation",
"horizon.mods.switch.registries": "adds blocks, items or other registry entries",
"horizon.mods.switch.restart": "Restart the game to apply %s: they add blocks, items or world generation.",
"horizon.shader_packs.empty": "No shader packs: put them in the shaderpacks folder",
"horizon.shader_packs.kind_horizon": " (Horizon)",
"horizon.shader_options.open": "Shader Options...",
@@ -302,6 +325,7 @@
"horizon.extras.splitscreen": "Splitscreen",
"horizon.extras.chat_animation": "Smooth chat",
"horizon.extras.chat_indicator": "Message indicator",
"horizon.extras.wheel_cycles_list_settings.tooltip": "The scroll wheel over a button in a settings list changes its value. Off, as in vanilla, the wheel scrolls the list.",
"horizon.extras.wheel_cycles_settings": "Scroll wheel changes settings",
"horizon.extras.pack_browser": "Resource pack browser",
"horizon.extras.fullbright.tooltip": "Show blocks and entities at full light, even in dark areas.",
@@ -374,7 +398,6 @@
"horizon.osk.close": "Close",
"horizon.extras.chat_animation.tooltip": "A new chat line slides in from the left while it fades in, instead of appearing whole.",
"horizon.extras.chat_indicator.tooltip": "The coloured bar at the left of a chat line.",
"horizon.extras.wheel_cycles_settings.tooltip": "The scroll wheel over a button in the settings changes its value, as in vanilla. Off, the wheel only scrolls the list.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs gets an Add button that searches Modrinth and installs a pack into your packs folder. Off, the game never contacts Modrinth.",
"horizon.extras.splitscreen.tooltip": "In a world, another controller can join as a new player: press A when it asks, then pick a profile and where to play.",
"horizon.splitscreen.setup.monitor": "Monitor %s",
@@ -563,8 +586,8 @@
"horizon.emote_wheel.help": "Release or click to play · 1–8 · Esc closes",
"horizon.extras.cosmetics_others": "Other players' cosmetics",
"horizon.extras.cosmetics_others.tooltip": "What other Horizon players wear: everything, only their name tag styles, or nothing. Hidden unless you own cosmetics, until you choose. Hidden, the game never asks what they wear.",
"horizon.extras.cosmetics_auras": "Others' auras and trails",
"horizon.extras.cosmetics_auras.tooltip": "Auras and trails other players wear. Off by default.",
"horizon.extras.cosmetics_hide_fx": "Hide other players' trails and auras",
"horizon.extras.cosmetics_hide_fx.tooltip": "Other players' trails and auras stay hidden; their other cosmetics still show. Off by default.",
"horizon.extras.cosmetics.shown": "Shown",
"horizon.extras.cosmetics.hidden": "Hidden",
"horizon.extras.cosmetics.name_tags": "Name tags only",
@@ -790,7 +813,24 @@
"horizon.pvp.chat_search.tooltip": "Ctrl+F in the open chat: what you type shows only the messages holding it. Enter or Ctrl+F again stops.",
"horizon.pvp.preset": "PvP preset",
"horizon.pvp.preset.tooltip": "Turns on the custom crosshair, the armour and effects widgets, toggle sprint and sneak, lower fire, and the FPS counter and keystrokes mods if installed. Your settings for each stay.",
"horizon.pvp.hud_editor_key": "HUD editor key",
"horizon.pvp.hud_editor_key.tooltip": "Opens the HUD editor from the game. Click, then press a key; Escape unbinds it.",
"horizon.pvp.editor": "Edit HUD",
"horizon.pvp.editor.tooltip": "Move the widgets on the game: drag them anywhere, resize them with the wheel, right click one to put it back in its corner.",
"horizon.pvp.editor.hint": "Drag widgets · Wheel: size · Right click: back to its corner",
"horizon.pvp.combat_feel": "Combat feel",
"horizon.pvp.combat_feel.tooltip": "Legacy draws fights as 1.8.9 did: the item in your hand, its swing, eating, the bow and a sword held up while you hold use, without the dip after each hit or the attack indicator. Only the look: what you send a server stays the same.",
"horizon.pvp.combat_indicator": "Attack indicator in Legacy",
"horizon.pvp.combat_indicator.tooltip": "Keeps the attack indicator under Legacy, which 1.8.9 didn't have.",
"horizon.pvp.combat_feel.modern": "Modern",
"horizon.pvp.combat_feel.legacy": "Legacy (1.8.9)",
"horizon.pvp.header.combat": "Combat feel",
"horizon.pvp.combat_sounds": "1.8 hit sounds",
"horizon.pvp.combat_sounds.tooltip": "Under Legacy, leaves out the attack sounds 1.9 added (strong, weak, sweep, critical...). Hurt sounds stay.",
"horizon.pvp.combat_rules": "1.8 combat in worlds you host",
"horizon.pvp.combat_rules.tooltip": "Single player and the worlds you open to LAN fight by 1.8.9's rules: no attack cooldown or sweep, 1.8 weapon damage, Strength, Sharpness and critical hits. Servers you join keep their own rules.",
"Bindings": "Bindings",
"Play": "Play",
"Device": "Device",
"Look": "Look",
"Movement": "Movement",
@@ -812,6 +852,7 @@
"How far the right stick moves before the view turns. Raise it if the view drifts.": "How far the right stick moves before the view turns. Raise it if the view drifts.",
"Hold the button to sneak, or press it once to start and again to stop.": "Hold the button to sneak, or press it once to start and again to stop.",
"How far the left stick moves before you walk. Raise it if you drift.": "How far the left stick moves before you walk. Raise it if you drift.",
"How far a trigger is pulled before it counts as pressed.": "How far a trigger is pulled before it counts as pressed.",
"How fast the stick moves the crosshair in inventories.": "How fast the stick moves the crosshair in inventories.",
"How fast the right stick scrolls a list.": "How fast the right stick scrolls a list.",
"How long a held direction waits before it repeats.": "How long a held direction waits before it repeats.",
@@ -322,7 +322,6 @@
"horizon.osk.close": "Fermu",
"horizon.extras.chat_animation.tooltip": "Nova babillinio glitas enen de maldekstre dum ĝi fadas, anstataŭ aperi tute.",
"horizon.extras.chat_indicator.tooltip": "La kolora stango maldekstre de babillinio.",
"horizon.extras.wheel_cycles_settings.tooltip": "La rula rado super butono en la agordoj ŝanĝas sian valoron, kiel en vanilo. Malŝaltita, la rado nur rulumas la liston.",
"horizon.extras.pack_browser.tooltip": "Elektu Rimedo-Pakojn ricevas butonon Aldoni, kiu serĉas Modrinth kaj instalas pakon en viajn pakajn dosierujojn. Malŝaltita, la ludo neniam kontaktas Modrinth.",
"horizon.extras.splitscreen.tooltip": "En mondo, alia regilo povas aliĝi kiel nova ludanto: premu A kiam ĝi petas, tiam elektu profilon kaj kie ludi.",
"horizon.splitscreen.setup.monitor": "Monitoro %s",
@@ -468,8 +467,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Kaŝita: ĉiu Horizon-ludanto vidas vin sen viaj kosmetikaĵoj, vi inkluzivis. La sama ŝaltilo kiel la Vestoŝranko de la lanĉilo.",
"horizon.extras.cosmetics_others": "Kosmetikaĵoj de aliaj ludantoj",
"horizon.extras.cosmetics_others.tooltip": "Kion aliaj Horizon ludantoj portas: ĉion, nur siajn nometikedstilojn, aŭ nenion. Kaŝita, la ludo neniam demandas kion ili portas.",
"horizon.extras.cosmetics_auras": "Aŭroj kaj spuroj de aliaj",
"horizon.extras.cosmetics_auras.tooltip": "Aŭroj kaj spuroj kiujn aliaj ludantoj portas. Malŝaltita defaŭlte.",
"horizon.extras.cosmetics_hide_fx": "Kaŝi spurojn kaj aŭrojn de aliaj ludantoj",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spuroj kaj aŭroj de aliaj ludantoj restas kaŝitaj; iliaj aliaj kosmetikaĵoj ankoraŭ montriĝas. Defaŭlte malŝaltita.",
"horizon.extras.cosmetics.shown": "Montrita",
"horizon.extras.cosmetics.hidden": "Kaŝita",
"horizon.extras.cosmetics.name_tags": "Nomaj etikedoj nur"
@@ -321,7 +321,6 @@
"horizon.osk.close": "Sule",
"horizon.extras.chat_animation.tooltip": "Uus vestlusliin libiseb vasakult sisse, kui see sumbub, selle asemel, et paista tervena.",
"horizon.extras.chat_indicator.tooltip": "Vestlusrea vasakul olev värviline riba.",
"horizon.extras.wheel_cycles_settings.tooltip": "Seadetes nupu kohal olev kerimisratas muudab selle väärtust, nagu vanilje puhul. Väljas, ratas kerib ainult loendit.",
"horizon.extras.pack_browser.tooltip": "Ressursipaketi valikul kuvatakse nupp Lisa, mis otsib Modrinthi ja installib paketi teie pakettide kausta. Väljas, mäng ei võta kunagi Modrinthiga ühendust.",
"horizon.extras.splitscreen.tooltip": "Maailmas saab uue mängijana liituda teine kontroller: vajutage A, kui see küsib, seejärel valige profiil ja kus mängida.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
@@ -464,8 +463,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Varjatud: iga Horizoni mängija näeb teid ilma teie kosmeetikata, kaasa arvatud. Sama lüliti, mis käivitaja riidekapil.",
"horizon.extras.cosmetics_others": "Teiste mängijate kosmeetika",
"horizon.extras.cosmetics_others.tooltip": "Mida teised Horizoni mängijad kannavad: kõike, ainult nende nimesiltide stiile või mitte midagi. Varjatud, mäng ei küsi kunagi, mida nad kannavad.",
"horizon.extras.cosmetics_auras": "Teiste aurad ja jäljed",
"horizon.extras.cosmetics_auras.tooltip": "Aurad ja jäljed, mida teised mängijad kannavad. Vaikimisi väljas.",
"horizon.extras.cosmetics_hide_fx": "Peida teiste mängijate jäljed ja aurad",
"horizon.extras.cosmetics_hide_fx.tooltip": "Teiste mängijate jäljed ja aurad jäävad peidetuks; nende muu kosmeetika on endiselt näha. Vaikimisi väljas.",
"horizon.extras.cosmetics.shown": "Näidatud",
"horizon.extras.cosmetics.hidden": "Peidetud",
"horizon.extras.cosmetics.name_tags": "Ainult nimesildid"
@@ -323,7 +323,6 @@
"horizon.osk.close": "Itxi",
"horizon.extras.chat_animation.tooltip": "Txat-lerro berri bat ezkerretik sartzen da desagertzen den bitartean, osorik agertu beharrean.",
"horizon.extras.chat_indicator.tooltip": "Txat-lerro baten ezkerreko koloretako barra.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ezarpenetako botoi baten gainean korritzeko gurpilak bere balioa aldatzen du, bainilan bezala. Desaktibatuta, gurpilak zerrendan bakarrik mugitzen du.",
"horizon.extras.pack_browser.tooltip": "Aukeratu Baliabide Paketeak Gehitu botoi bat lortzen du, Modrinth bilatzen duena eta pakete bat instalatzen du zure paketeen karpetan. Desaktibatuta, jokoa ez da inoiz Modrinth-ekin harremanetan jartzen.",
"horizon.extras.splitscreen.tooltip": "Mundu batean, beste kontrolagailu bat sar daiteke jokalari berri gisa: sakatu A eskatzen dionean, eta hautatu profil bat eta non jokatu.",
"horizon.splitscreen.setup.monitor": "Monitorea %s",
@@ -469,8 +468,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Ezkutuan: Horizon jokalari bakoitzak zure kosmetikarik gabe ikusten zaitu, zuk barne. Abiarazlearen armairuaren etengailu bera.",
"horizon.extras.cosmetics_others": "Beste jokalarien kosmetika",
"horizon.extras.cosmetics_others.tooltip": "Beste Horizon jokalariek zer janzten duten: denetarik, izenaren etiketa estiloak bakarrik, edo ezer ez. Ezkutuan, jokoak ez du inoiz galdetzen zer janzten duten.",
"horizon.extras.cosmetics_auras": "Besteen aura eta ibilbideak",
"horizon.extras.cosmetics_auras.tooltip": "Beste jokalariek janzten dituzten aura eta arrastoak. Desaktibatuta lehenespenez.",
"horizon.extras.cosmetics_hide_fx": "Ezkutatu beste jokalarien arrastoak eta aurak",
"horizon.extras.cosmetics_hide_fx.tooltip": "Beste jokalarien arrastoak eta aurak ezkutuan geratzen dira; haien beste kosmetikoak ikusten jarraitzen dute. Desaktibatuta lehenespenez.",
"horizon.extras.cosmetics.shown": "Erakutsita",
"horizon.extras.cosmetics.hidden": "Ezkutuan",
"horizon.extras.cosmetics.name_tags": "Izena etiketak soilik"
@@ -322,7 +322,6 @@
"horizon.osk.close": "بستن",
"horizon.extras.chat_animation.tooltip": "یک خط چت جدید به جای اینکه کامل نشان داده شود، از سمت چپ وارد می شود و در حالی که محو می شود.",
"horizon.extras.chat_indicator.tooltip": "نوار رنگی در سمت چپ یک خط چت.",
"horizon.extras.wheel_cycles_settings.tooltip": "چرخ اسکرول روی یک دکمه در تنظیمات مقدار آن را تغییر می دهد، مانند وانیل. خاموش، چرخ فقط فهرست را پیمایش می کند.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs یک دکمه Add دریافت می کند که Modrinth را جستجو می کند و یک بسته را در پوشه بسته های شما نصب می کند. خاموش، بازی هرگز با Modrinth تماس نمی گیرد.",
"horizon.extras.splitscreen.tooltip": "در یک دنیا، کنترل‌کننده دیگری می‌تواند به‌عنوان یک بازیکن جدید ملحق شود: وقتی A را درخواست کرد، A را فشار دهید، سپس یک نمایه و مکان بازی را انتخاب کنید.",
"horizon.splitscreen.setup.monitor": "مانیتور %s",
@@ -474,8 +473,8 @@
"horizon.extras.cosmetics_mine.tooltip": "پنهان: هر بازیکن Horizon شما را بدون لوازم آرایشی شما، از جمله شما، می بیند. همان سوئیچ کمد لانچر.",
"horizon.extras.cosmetics_others": "لوازم آرایشی دیگر بازیکنان",
"horizon.extras.cosmetics_others.tooltip": "سایر بازیکنان Horizon چه می پوشند: همه چیز، فقط سبک برچسب نام آنها یا هیچ چیز. پنهان، بازی هرگز نمی پرسد آنها چه می پوشند.",
"horizon.extras.cosmetics_auras": "هاله ها و مسیرهای دیگران",
"horizon.extras.cosmetics_auras.tooltip": "هاله ها و دنباله های دیگر بازیکنان می پوشند. به طور پیش فرض خاموش است.",
"horizon.extras.cosmetics_hide_fx": "پنهان کردن ردها و هاله‌های بازیکنان دیگر",
"horizon.extras.cosmetics_hide_fx.tooltip": "ردها و هاله‌های بازیکنان دیگر پنهان می‌مانند؛ دیگر آرایه‌هایشان همچنان دیده می‌شوند. به‌طور پیش‌فرض خاموش.",
"horizon.extras.cosmetics.shown": "نشان داده شده است",
"horizon.extras.cosmetics.hidden": "پنهان شده است",
"horizon.extras.cosmetics.name_tags": "فقط برچسب های نام"
@@ -306,7 +306,6 @@
"horizon.osk.close": "Sulje",
"horizon.extras.chat_animation.tooltip": "Uusi chat-linja liukuu sisään vasemmalta sen häipyessä sen sijaan, että se näyttäisi kokonaisena.",
"horizon.extras.chat_indicator.tooltip": "Värillinen palkki keskustelupalkin vasemmalla puolella.",
"horizon.extras.wheel_cycles_settings.tooltip": "Asetuksissa painikkeen päällä oleva vierityspyörä muuttaa sen arvoa, kuten vaniljassa. Pois päältä, pyörä vain vierittää luetteloa.",
"horizon.extras.pack_browser.tooltip": "Valitse Resurssipaketit saa Lisää-painikkeen, joka etsii Modrinthista ja asentaa paketin pakettikansioosi. Pois päältä, peli ei koskaan saa yhteyttä Modrinthiin.",
"horizon.extras.splitscreen.tooltip": "Maailmassa toinen ohjain voi liittyä uutena pelaajana: paina A, kun se pyytää, ja valitse sitten profiili ja pelipaikka.",
"horizon.splitscreen.setup.monitor": "Monitori %s",
@@ -449,8 +448,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Piilotettu: jokainen Horizon-pelaaja näkee sinut ilman kosmetiikkaasi, sinä mukaan lukien. Sama kytkin kuin kantoraketin vaatekaappi.",
"horizon.extras.cosmetics_others": "Muiden pelaajien kosmetiikkaa",
"horizon.extras.cosmetics_others.tooltip": "Mitä muut Horizon-pelaajat käyttävät: kaikki, vain heidän nimilaputyylinsä tai ei mitään. Piilotettu, peli ei koskaan kysy, mitä he käyttävät.",
"horizon.extras.cosmetics_auras": "Toisten aurat ja polut",
"horizon.extras.cosmetics_auras.tooltip": "Aurat ja jäljet, joita muut pelaajat käyttävät. Oletuksena pois päältä.",
"horizon.extras.cosmetics_hide_fx": "Piilota muiden pelaajien jäljet ja aurat",
"horizon.extras.cosmetics_hide_fx.tooltip": "Muiden pelaajien jäljet ja aurat pysyvät piilossa; heidän muut kosmetiikkansa näkyvät yhä. Oletuksena pois.",
"horizon.extras.cosmetics.shown": "Näytetään",
"horizon.extras.cosmetics.hidden": "Piilotettu",
"horizon.extras.cosmetics.name_tags": "Vain nimilaput",
@@ -285,7 +285,6 @@
"horizon.osk.close": "Isara",
"horizon.extras.chat_animation.tooltip": "Ang isang bagong linya ng chat ay dumudulas mula sa kaliwa habang ito ay nawawala, sa halip na lumabas nang buo.",
"horizon.extras.chat_indicator.tooltip": "Ang may kulay na bar sa kaliwa ng isang chat line.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ang scroll wheel sa ibabaw ng isang button sa mga setting ay nagbabago ng halaga nito, tulad ng sa vanilla. Naka-off, ini-scroll lang ng gulong ang listahan.",
"horizon.extras.pack_browser.tooltip": "Ang Select Resource Packs ay nakakakuha ng Add button na naghahanap sa Modrinth at nag-i-install ng pack sa iyong pack folder. Wala, ang laro ay hindi kailanman nakikipag-ugnayan sa Modrinth.",
"horizon.extras.splitscreen.tooltip": "Sa isang mundo, maaaring sumali ang isa pang controller bilang bagong player: pindutin ang A kapag nagtanong ito, pagkatapos ay pumili ng profile at kung saan maglaro.",
"horizon.splitscreen.setup.monitor": "Subaybayan %s",
@@ -421,8 +420,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Nakatago: makikita ka ng bawat Horizon na manlalaro nang wala ang iyong mga pampaganda, kasama mo. Ang parehong switch bilang Wardrobe ng launcher.",
"horizon.extras.cosmetics_others": "Mga pampaganda ng ibang manlalaro",
"horizon.extras.cosmetics_others.tooltip": "Ano ang isinusuot ng iba pang Horizon na manlalaro: lahat, mga istilo lang ng name tag nila, o wala. Nakatago, ang laro ay hindi nagtatanong kung ano ang kanilang isinusuot.",
"horizon.extras.cosmetics_auras": "Mga aura at landas ng iba",
"horizon.extras.cosmetics_auras.tooltip": "Mga aura at trail na isinusuot ng ibang mga manlalaro. Naka-off bilang default.",
"horizon.extras.cosmetics_hide_fx": "Itago ang mga bakas at aura ng ibang manlalaro",
"horizon.extras.cosmetics_hide_fx.tooltip": "Nakatago ang mga bakas at aura ng ibang manlalaro; nakikita pa rin ang iba nilang cosmetics. Naka-off bilang default.",
"horizon.extras.cosmetics.shown": "Ipinakita",
"horizon.extras.cosmetics.hidden": "Nakatago",
"horizon.extras.cosmetics.name_tags": "Mga name tag lang",
@@ -319,7 +319,6 @@
"horizon.osk.close": "Lat aftur",
"horizon.extras.chat_animation.tooltip": "Ein nýggj kjaklinja rennur inn frá vinstru, meðan hon doyvir inn, í staðin fyri at síggja heil út.",
"horizon.extras.chat_indicator.tooltip": "Litaða strikan til vinstru á kjaklinju.",
"horizon.extras.wheel_cycles_settings.tooltip": "Rulluhjólið yvir einum knøtti í innstillingunum broytir virðið, eins og í vanilju. Av, hjólið rullar bara listan.",
"horizon.extras.pack_browser.tooltip": "Vel Tilfeingispakkar fær ein Legg til knøtt, sum leitar Modrinth og setur ein pakka upp í tína pakka mappu. Sløkt, spælið ongantíð kontaktar Modrinth.",
"horizon.extras.splitscreen.tooltip": "Í einum heimi kann ein annar stýrari melda seg til sum nýggjur spælari: trýst á A tá tað spyr, vel síðani ein profil og hvar tú skalt spæla.",
"horizon.splitscreen.setup.monitor": "Skjár %s",
@@ -471,8 +470,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Dult: hvør Horizon spælari sær teg uttan títt kosmetikk, tú eisini. Sama rotari sum Klædnaskápið hjá sjósetaranum.",
"horizon.extras.cosmetics_others": "Kosmetikkur hjá øðrum spælarum",
"horizon.extras.cosmetics_others.tooltip": "Hvat aðrir Horizon spælarar eru í: alt, bara teirra navnamerkjastílar, ella einki. Dult spyr spælið ongantíð, hvat tey eru í.",
"horizon.extras.cosmetics_auras": "Aura og slóðir hjá øðrum",
"horizon.extras.cosmetics_auras.tooltip": "Auras og slóðir aðrir spælarar bera. Sløkt sum standard.",
"horizon.extras.cosmetics_hide_fx": "Fjal spor og aurur hjá øðrum leikarum",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spor og aurur hjá øðrum leikarum verða fjald; teirra aðrir prýðislutir síggjast framvegis. Sløkt sum standard.",
"horizon.extras.cosmetics.shown": "Víst",
"horizon.extras.cosmetics.hidden": "Dult",
"horizon.extras.cosmetics.name_tags": "Einans navnamerki"
@@ -46,6 +46,28 @@
"horizon.mods.dev": " (dév.)",
"horizon.mods.settings": "Options",
"horizon.mods.open_folder": "Ouvrir le dossier des mods",
"horizon.mods.browse": "Parcourir les mods...",
"horizon.mods.browse.title": "Parcourir les mods",
"horizon.mods.browse.search": "Rechercher des mods...",
"horizon.mods.browse.project": "Mod",
"horizon.mods.browse.loading": "Lecture du registre des mods Horizon...",
"horizon.mods.browse.failed": "Impossible de lire le registre des mods : %s",
"horizon.mods.browse.none": "Aucun mod trouvé",
"horizon.mods.browse.has_update": " (mise à jour)",
"horizon.mods.browse.is_installed": " (installé)",
"horizon.mods.browse.by": "par %s",
"horizon.mods.browse.installing": "Installation...",
"horizon.mods.browse.install": "Installer",
"horizon.mods.browse.update": "Mettre à jour",
"horizon.mods.browse.installed": "Installé",
"horizon.mods.browse.open_page": "Ouvrir la page",
"horizon.mods.browse.enabled": "Mod",
"horizon.mods.browse.changelog": "Nouveautés",
"horizon.mods.browse.done": "%s %s est installé et activé.",
"horizon.mods.browse.install_failed": "Installation impossible : %s",
"horizon.mods.switch.worldgen": "change la génération du monde",
"horizon.mods.switch.registries": "ajoute des blocs, des objets ou d'autres entrées de registre",
"horizon.mods.switch.restart": "Redémarre le jeu pour appliquer %s : ils ajoutent des blocs, des objets ou de la génération de monde.",
"horizon.shader_packs.empty": "Aucun pack de shaders : placez-les dans le dossier shaderpacks",
"horizon.shader_options.open": "Options des shaders...",
"horizon.shader_options.profile": "Profil",
@@ -400,8 +422,8 @@
"horizon.emote_wheel.help": "Relâche ou clique pour jouer · 1–8 · Échap ferme",
"horizon.extras.cosmetics_others": "Cosmétiques des autres",
"horizon.extras.cosmetics_others.tooltip": "Ce que portent les autres joueurs Horizon : tout, seulement leur style d'étiquette de nom, ou rien. Masqués si vous n'avez pas de cosmétiques, tant que vous n'avez pas choisi. Masqués, le jeu ne demande jamais ce qu'ils portent.",
"horizon.extras.cosmetics_auras": "Auras et traînées des autres",
"horizon.extras.cosmetics_auras.tooltip": "Les auras et traînées que portent les autres joueurs. Désactivé par défaut.",
"horizon.extras.cosmetics_hide_fx": "Masquer les traînées et auras des autres joueurs",
"horizon.extras.cosmetics_hide_fx.tooltip": "Les traînées et auras des autres joueurs restent masquées ; leurs autres cosmétiques restent visibles. Désactivé par défaut.",
"horizon.extras.cosmetics.shown": "Affichés",
"horizon.extras.cosmetics.hidden": "Masqués",
"horizon.extras.cosmetics.name_tags": "Étiquettes seulement",
@@ -436,8 +458,8 @@
"Results for \"%s\"": "Résultats pour \"%s\"",
"No settings match.": "Aucun paramètre ne correspond.",
"horizon.extras.discord_presence": "Présence Discord",
"horizon.extras.wheel_cycles_list_settings.tooltip": "La molette sur un bouton d'une liste de paramètres modifie sa valeur. Désactivé, comme dans le jeu d'origine, la molette fait défiler la liste.",
"horizon.extras.wheel_cycles_settings": "La molette modifie les paramètres",
"horizon.extras.wheel_cycles_settings.tooltip": "La molette de défilement sur un bouton des paramètres modifie sa valeur, comme dans le jeu d'origine. Désactivé, la molette fait seulement défiler la liste.",
"Block Entities": "Entités de bloc",
"Shulker Boxes": "Boîtes de shulker",
"Reuses settled boxes; lid movement stays animated.": "Réutilise les boîtes immobiles ; le mouvement du couvercle reste animé.",
@@ -315,7 +315,6 @@
"horizon.osk.close": "Sierâ",
"horizon.extras.chat_animation.tooltip": "Une gnove linie di chat e slungje dentri di çampe intant che e svanìs, invezit di comparî interie.",
"horizon.extras.chat_indicator.tooltip": "La barre colorade a çampe di une rie di chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "La ruede di scroll suntun botôn intes impostazions e cambie il so valôr, come inte vanilie. Off, la ruede e scrole dome la liste.",
"horizon.extras.pack_browser.tooltip": "Selezionâ Pachetis di risorsis al oten un botôn Azontâ che al cîr Modrinth e al instale un pachete inte cjarte dai pachetis. Off, il zûc nol contat mai Modrinth.",
"horizon.extras.splitscreen.tooltip": "Intun mont, un altri controladôr al pues jentrâ come gnûf zuiadôr: premi A cuant che al domande, daspò sielzi un profîl e dulà zuiâ.",
"horizon.splitscreen.setup.monitor": "Monitôr %s",
@@ -467,8 +466,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Scuindût: ogni zuiadôr Horizon al viôt cence il to cosmetic, tu ancje. Il stes interrutôr dal Armadi dal lançadôr.",
"horizon.extras.cosmetics_others": "I cosmetics dai altris zuiadôrs",
"horizon.extras.cosmetics_others.tooltip": "Ce che a metin sù altris zuiadôrs Horizon: dut, dome i lôr stîi di etichetis, o nuie. Scuindût, il zûc nol domande mai ce che a metin sù.",
"horizon.extras.cosmetics_auras": "Auris e sentiers di altris",
"horizon.extras.cosmetics_auras.tooltip": "Auris e pistis che a metin sù altris zuiadôrs. Disactivât par automatic.",
"horizon.extras.cosmetics_hide_fx": "Plate lis olmis e lis aureis dai altris zuiadôrs",
"horizon.extras.cosmetics_hide_fx.tooltip": "Lis olmis e lis aureis dai altris zuiadôrs a restin platadis; i lôr altris cosmetics si viodin ancjemò. Distudât di predefinît.",
"horizon.extras.cosmetics.shown": "Mostrât",
"horizon.extras.cosmetics.hidden": "Scuindût",
"horizon.extras.cosmetics.name_tags": "Dome lis etichetis dai nons"
@@ -271,7 +271,6 @@
"horizon.osk.close": "Slút",
"horizon.extras.chat_animation.tooltip": "In nije petearrigel glydt fan links yn, wylst it yn ferdwynt, ynstee fan folslein te ferskinen.",
"horizon.extras.chat_indicator.tooltip": "De kleurde balke oan de linkerkant fan in petear line.",
"horizon.extras.wheel_cycles_settings.tooltip": "It scrollrad oer in knop yn 'e ynstellings feroaret syn wearde, lykas yn vanille. Off, it tsjil rôlet allinnich de list.",
"horizon.extras.pack_browser.tooltip": "Selektearje boarnepakketten krijt in knop taheakje dy't Modrinth siket en in pakket ynstalleart yn jo pakketmap. Ut, it spultsje nea kontakt Modrinth.",
"horizon.extras.splitscreen.tooltip": "Yn in wrâld kin in oare controller meidwaan as in nije spiler: druk op A as it freget, kies dan in profyl en wêr te spyljen.",
"horizon.splitscreen.setup.monitor_unknown": "in monitor",
@@ -399,8 +398,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Ferburgen: elke Horizon-spiler sjocht jo sûnder jo kosmetika, jo ynbegrepen. Deselde switch as de Launcher syn Wardrobe.",
"horizon.extras.cosmetics_others": "Oare spilers 'cosmetica",
"horizon.extras.cosmetics_others.tooltip": "Wat oare Horizon-spilers drage: alles, allinich har nammelabelstilen, of neat. Ferburgen, it spultsje freget noait wat se drage.",
"horizon.extras.cosmetics_auras": "Oare aura's en spoaren",
"horizon.extras.cosmetics_auras.tooltip": "Auras en spoaren oare spilers drage. Standert út.",
"horizon.extras.cosmetics_hide_fx": "Spoaren en aura's fan oare spilers ferbergje",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spoaren en aura's fan oare spilers bliuwe ferburgen; harren oare kosmetika bliuwt sichtber. Standert út.",
"horizon.extras.cosmetics.shown": "Oanjûn",
"horizon.extras.cosmetics.hidden": "Ferburgen",
"horizon.extras.cosmetics.name_tags": "Allinnich nammelabels"
@@ -319,7 +319,6 @@
"horizon.osk.close": "Dún",
"horizon.extras.chat_animation.tooltip": "Sleamhnaíonn líne comhrá nua isteach ón taobh clé agus í ag dul i léig, in ionad a bheith ina hiomláine.",
"horizon.extras.chat_indicator.tooltip": "An barra daite ar thaobh na láimhe clé den líne chomhrá.",
"horizon.extras.wheel_cycles_settings.tooltip": "Athraíonn an roth scrollaithe thar chnaipe sna socruithe a luach, mar atá i vanilla. Lasmuigh de, ní scrollaíonn an roth ach an liosta.",
"horizon.extras.pack_browser.tooltip": "Faigheann Roghnaigh Acmhainne Pacáistí cnaipe Cuir leis a chuardaíonn Modrinth agus a shuiteálann pacáiste isteach i d’fhillteán pacaí. Lasmuigh de, ní théann an cluiche i dteagmháil le Modrinth riamh.",
"horizon.extras.splitscreen.tooltip": "I ndomhan, is féidir le rialtóir eile a bheith páirteach mar imreoir nua: brúigh A nuair a iarrann sé, ansin roghnaigh próifíl agus cá háit le himirt.",
"horizon.splitscreen.setup.monitor": "Monatóir %s",
@@ -463,8 +462,8 @@
"horizon.extras.cosmetics_mine.tooltip": "I bhfolach: feiceann gach imreoirHorizon tú gan do chosmaidí, tú san áireamh. An lasc céanna le Wardrobe an tosaitheoir.",
"horizon.extras.cosmetics_others": "Cosmaidí imreoirí eile",
"horizon.extras.cosmetics_others.tooltip": "Cad a chaitheann imreoirí Horizon eile: gach rud, gan ach a stíleanna clibeanna ainm, nó rud ar bith. I bhfolach, ní iarrann an cluiche riamh cad a chaitheann siad.",
"horizon.extras.cosmetics_auras": "Auras agus cosáin daoine eile",
"horizon.extras.cosmetics_auras.tooltip": "Auras agus rianta a chaitheamh imreoirí eile. Off de réir réamhshocraithe.",
"horizon.extras.cosmetics_hide_fx": "Folaigh rianta agus aurataí imreoirí eile",
"horizon.extras.cosmetics_hide_fx.tooltip": "Fanann rianta agus aurataí imreoirí eile i bhfolach; feictear a gcuid maisithe eile fós. As de réir réamhshocraithe.",
"horizon.extras.cosmetics.shown": "Taispeántar",
"horizon.extras.cosmetics.hidden": "i bhfolach",
"horizon.extras.cosmetics.name_tags": "Clibeanna ainm amháin"
@@ -320,7 +320,6 @@
"horizon.osk.close": "Dùin",
"horizon.extras.chat_animation.tooltip": "Bidh loidhne cabadaich ùr a’ sleamhnachadh a-steach bhon taobh chlì fhad ‘s a tha e a’ seargadh a-steach, an àite a bhith a ’nochdadh slàn.",
"horizon.extras.chat_indicator.tooltip": "Am bàr dathte air taobh clì loidhne cabadaich.",
"horizon.extras.wheel_cycles_settings.tooltip": "Bidh a’ chuibhle scrollaidh thairis air putan anns na roghainnean ag atharrachadh a luach, mar ann am vanilla. Air falbh, cha sgrolaich a’ chuibhle ach an liosta.",
"horizon.extras.pack_browser.tooltip": "Gheibh Tagh Pacaidean Goireasan putan Cuir ris a nì sgrùdadh air Modrinth agus a chuireas pacaid a-steach don phasgan pacaidean agad. Air falbh, cha chuir an geama fios gu Modrinth a-riamh.",
"horizon.extras.splitscreen.tooltip": "Ann an saoghal, faodaidh rianadair eile a dhol còmhla mar chluicheadair ùr: brùth A nuair a dh’ iarras e, an uairsin tagh ìomhaigh agus càite an cluich thu.",
"horizon.splitscreen.setup.monitor_unknown": "sgrùdair",
@@ -464,8 +463,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Falaichte: chì a h-uile cluicheadair Horizon thu às aonais na cungaidhean maise agad, thug thu a-steach. An aon tionndadh ri Wardrobe an lannsair.",
"horizon.extras.cosmetics_others": "Coltas cluicheadairean eile",
"horizon.extras.cosmetics_others.tooltip": "Na bhios cluicheadairean Horizon eile a’ caitheamh: a h-uile càil, dìreach na stoidhlichean tag ainm aca, no dad. Falaichte, cha bhith an geama a-riamh a’ faighneachd dè a bhios orra.",
"horizon.extras.cosmetics_auras": "Auras agus slighean cuid eile",
"horizon.extras.cosmetics_auras.tooltip": "Auras agus slighean bidh cluicheadairean eile a’ caitheamh. Cuir dheth gu bunaiteach.",
"horizon.extras.cosmetics_hide_fx": "Falaich slighean is aurathan chluicheadairean eile",
"horizon.extras.cosmetics_hide_fx.tooltip": "Fanaidh slighean is aurathan chluicheadairean eile falaichte; chithear an cuid maise eile fhathast. Dheth a ghnàth.",
"horizon.extras.cosmetics.shown": "Air a shealltainn",
"horizon.extras.cosmetics.hidden": "Falaichte",
"horizon.extras.cosmetics.name_tags": "Tagaichean ainmean a-mhàin"
@@ -318,7 +318,6 @@
"horizon.osk.close": "Pechar",
"horizon.extras.chat_animation.tooltip": "Unha nova liña de chat deslízase dende a esquerda mentres se esvae, en lugar de aparecer enteira.",
"horizon.extras.chat_indicator.tooltip": "A barra de cores á esquerda dunha liña de chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "A roda de desprazamento sobre un botón na configuración cambia o seu valor, como en vainilla. Desactivada, a roda só despraza a lista.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs obtén un botón Engadir que busca en Modrinth e instala un paquete no cartafol de paquetes. Desactivado, o xogo nunca contacta con Modrinth.",
"horizon.extras.splitscreen.tooltip": "Nun mundo, outro controlador pode unirse como xogador novo: preme A cando o solicite, despois escolle un perfil e onde xogar.",
"horizon.splitscreen.setup.monitor_unknown": "un monitor",
@@ -463,8 +462,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Oculto: todos os xogadores de Horizon venche sen os teus cosméticos, ti incluído. O mesmo interruptor que o garda-roupa do lanzador.",
"horizon.extras.cosmetics_others": "Cosmética doutros xogadores",
"horizon.extras.cosmetics_others.tooltip": "Que usan outros Horizon xogadores: todo, só os seus estilos de etiqueta de nome ou nada. Oculto, o xogo nunca pregunta que visten.",
"horizon.extras.cosmetics_auras": "Auras e sendeiros doutros",
"horizon.extras.cosmetics_auras.tooltip": "Auras e rastros que levan outros xogadores. Desactivado por defecto.",
"horizon.extras.cosmetics_hide_fx": "Agochar as estelas e auras doutros xogadores",
"horizon.extras.cosmetics_hide_fx.tooltip": "As estelas e auras doutros xogadores quedan agochadas; os seus outros cosméticos séguense vendo. Desactivado por defecto.",
"horizon.extras.cosmetics.shown": "Amosado",
"horizon.extras.cosmetics.hidden": "Oculto",
"horizon.extras.cosmetics.name_tags": "Só etiquetas de nome"
@@ -315,7 +315,6 @@
"horizon.osk.close": "Pani",
"horizon.extras.chat_animation.tooltip": "He laina kamaʻilio hou e paheʻe mai ka hema i ka nalo ʻana, ma kahi o ka ʻike ʻia ʻana.",
"horizon.extras.chat_indicator.tooltip": "ʻO ka pahu kala ma ka hema o kahi laina kamaʻilio.",
"horizon.extras.wheel_cycles_settings.tooltip": "ʻO ka huila ʻōwili ma luna o kahi pihi ma nā hoʻonohonoho e hoʻololi i kona waiwai, e like me ka vanilla. Paʻa, ʻōwili wale ka huila i ka papa inoa.",
"horizon.extras.pack_browser.tooltip": "E koho i nā Pūke Punawai Loaʻa ke pihi Hoʻohui e ʻimi ana iā Modrinth a hoʻokomo i kahi pūʻolo i loko o kāu waihona waihona. Paʻa, ʻaʻole pili ka pāʻani iā Modrinth.",
"horizon.extras.splitscreen.tooltip": "I loko o ke ao, hiki i kekahi mea hoʻoponopono ke hui pū me he mea pāʻani hou: e kaomi iā A ke nīnau ʻo ia, a laila koho i kahi ʻaoʻao a me kahi e pāʻani ai.",
"horizon.splitscreen.setup.monitor": "Nānā %s",
@@ -458,8 +457,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Huna: ʻike kēlā me kēia Horizon iā ʻoe me ka ʻole o kāu mea hoʻonaninani, ua hoʻokomo ʻoe. ʻO ka hoʻololi like e like me ka mea hoʻolele lole.",
"horizon.extras.cosmetics_others": "ʻO nā mea hoʻonani o nā mea pāʻani ʻē aʻe",
"horizon.extras.cosmetics_others.tooltip": "He aha nā mea pāʻani Horizon ʻē aʻe e ʻaʻahu ai: nā mea āpau, nā ʻano inoa inoa wale nō, a ʻaʻohe mea. Huna, ʻaʻole nīnau ka pāʻani i kā lākou lole.",
"horizon.extras.cosmetics_auras": "ʻO nā aura a me nā alahele o nā poʻe ʻē aʻe",
"horizon.extras.cosmetics_auras.tooltip": "ʻO Auras a me nā alahele e hoʻohana ai nā mea pāʻani. Paʻa ma ka paʻamau.",
"horizon.extras.cosmetics_hide_fx": "Hūnā i nā meheu a me nā aura o nā mea pāʻani ʻē aʻe",
"horizon.extras.cosmetics_hide_fx.tooltip": "Hūnā ʻia nā meheu a me nā aura o nā mea pāʻani ʻē aʻe; ʻike ʻia nō kā lākou mau mea hoʻonani ʻē aʻe. Pio ma ke ʻano maʻamau.",
"horizon.extras.cosmetics.shown": "Hōʻike ʻia",
"horizon.extras.cosmetics.hidden": "Huna",
"horizon.extras.cosmetics.name_tags": "Nā inoa inoa wale nō"
@@ -321,7 +321,6 @@
"horizon.osk.close": "סגור",
"horizon.extras.chat_animation.tooltip": "קו צ'אט חדש גולש פנימה משמאל בזמן שהוא נמוג פנימה, במקום להיראות שלם.",
"horizon.extras.chat_indicator.tooltip": "הסרגל הצבעוני בצד שמאל של קו צ'אט.",
"horizon.extras.wheel_cycles_settings.tooltip": "גלגל הגלילה מעל כפתור בהגדרות משנה את ערכו, כמו בוניל. כבוי, הגלגל רק גולל את הרשימה.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs מקבל כפתור הוסף שמחפש את Modrinth ומתקין חבילה בתיקיית החבילות שלך. כבוי, המשחק אף פעם לא יוצר קשר עם Modrinth.",
"horizon.extras.splitscreen.tooltip": "בעולם, בקר אחר יכול להצטרף כשחקן חדש: הקש A כאשר הוא שואל, ואז בחר פרופיל ואיפה לשחק.",
"horizon.splitscreen.setup.monitor": "צג %s",
@@ -465,8 +464,8 @@
"horizon.extras.cosmetics_mine": "הקוסמטיקה שלי",
"horizon.extras.cosmetics_others": "מוצרי קוסמטיקה של שחקנים אחרים",
"horizon.extras.cosmetics_others.tooltip": "מה שחקני Horizon אחרים לובשים: הכל, רק סגנונות תג השם שלהם, או כלום. נסתר, המשחק אף פעם לא שואל מה הם לובשים.",
"horizon.extras.cosmetics_auras": "ההילות והשבילים של אחרים",
"horizon.extras.cosmetics_auras.tooltip": "הילות ושבילים ששחקנים אחרים לובשים. כבוי כברירת מחדל.",
"horizon.extras.cosmetics_hide_fx": "הסתרת שבילים והילות של שחקנים אחרים",
"horizon.extras.cosmetics_hide_fx.tooltip": "השבילים וההילות של שחקנים אחרים נשארים מוסתרים; שאר הקוסמטיקה שלהם עדיין מוצגת. כבוי כברירת מחדל.",
"horizon.extras.cosmetics.shown": "מוצג",
"horizon.extras.cosmetics.hidden": "מוסתר",
"horizon.extras.cosmetics.name_tags": "תגי שם בלבד",
@@ -312,7 +312,6 @@
"horizon.osk.close": "बंद करें",
"horizon.extras.chat_animation.tooltip": "एक नई चैट लाइन पूरी तरह से प्रकट होने के बजाय फीका पड़ने के दौरान बाईं ओर से स्लाइड करती है।",
"horizon.extras.chat_indicator.tooltip": "चैट लाइन के बाईं ओर रंगीन पट्टी।",
"horizon.extras.wheel_cycles_settings.tooltip": "सेटिंग्स में किसी बटन पर स्क्रॉल व्हील उसका मान बदलता है, जैसा कि वैनिला में होता है। बंद होने पर, व्हील केवल सूची को स्क्रॉल करता है।",
"horizon.extras.splitscreen.tooltip": "एक दुनिया में, एक अन्य कंट्रोलर नए खिलाड़ी के रूप में जुड़ सकता है: पूछने पर A दबाएं, फिर एक प्रोफ़ाइल चुनें और कहाँ खेलना है।",
"horizon.splitscreen.setup.monitor": "मॉनिटर %s",
"horizon.splitscreen.setup.monitor_unknown": "एक मॉनिटर",
@@ -306,7 +306,6 @@
"horizon.osk.close": "Zatvori",
"horizon.extras.chat_animation.tooltip": "Nova linija chata klizi s lijeve strane dok blijedi, umjesto da se pojavi cijela odjednom.",
"horizon.extras.chat_indicator.tooltip": "Traka u boji s lijeve strane linije chata.",
"horizon.extras.wheel_cycles_settings.tooltip": "Kotačić miša preko gumba u postavkama mijenja njegovu vrijednost, kao u vanila verziji. Kada je isključeno, kotačić samo pomiče popis.",
"horizon.extras.splitscreen.tooltip": "U svijetu se drugi upravljač može pridružiti kao novi igrač: pritisnite A kada se zatraži, zatim odaberite profil i mjesto igranja.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
"horizon.splitscreen.setup.window": "Prozor %s",
@@ -316,7 +316,6 @@
"horizon.osk.close": "Bezárás",
"horizon.extras.chat_animation.tooltip": "Egy új csevegősor balról csúszik be, miközben elhalványul, ahelyett, hogy egészben jelenne meg.",
"horizon.extras.chat_indicator.tooltip": "A színes sáv a csevegősor bal oldalán.",
"horizon.extras.wheel_cycles_settings.tooltip": "A beállításokban a gomb feletti görgő megváltoztatja annak értékét, mint a vanília esetében. Kikapcsolva a kerék csak a listát görgeti.",
"horizon.extras.pack_browser.tooltip": "Az Erőforrás-csomagok kiválasztásakor megjelenik egy Hozzáadás gomb, amely megkeresi a Modrinth-t, és telepít egy csomagot a csomagok mappájába. Ki, a játék soha nem lép kapcsolatba a Modrinth-szal.",
"horizon.extras.splitscreen.tooltip": "Egy világban egy másik kontroller is csatlakozhat új játékosként: nyomja meg az A gombot, amikor kéri, majd válasszon profilt és hol játszani.",
"horizon.splitscreen.setup.monitor_unknown": "egy monitort",
@@ -458,8 +457,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Rejtett: minden Horizon játékos lát téged a kozmetikumai nélkül, beleértve Önt is. Ugyanaz a kapcsoló, mint az indító gardróbjában.",
"horizon.extras.cosmetics_others": "Más játékosok kozmetikumai",
"horizon.extras.cosmetics_others.tooltip": "Amit a többi Horizon játékos visel: mindent, csak a névcímkék stílusát, vagy semmit. Rejtett, a játék soha nem kérdezi, mit viselnek.",
"horizon.extras.cosmetics_auras": "Mások aurái és nyomai",
"horizon.extras.cosmetics_auras.tooltip": "Más játékosok által viselt aurák és nyomvonalak. Alapértelmezés szerint ki.",
"horizon.extras.cosmetics_hide_fx": "Más játékosok nyomainak és auráinak elrejtése",
"horizon.extras.cosmetics_hide_fx.tooltip": "Más játékosok nyomai és aurái rejtve maradnak; a többi kozmetikumuk továbbra is látszik. Alapértelmezetten ki.",
"horizon.extras.cosmetics.shown": "Megjelenítve",
"horizon.extras.cosmetics.hidden": "Rejtett",
"horizon.extras.cosmetics.name_tags": "Csak névcímkék"
@@ -317,7 +317,6 @@
"horizon.osk.close": "Փակել",
"horizon.extras.chat_animation.tooltip": "Նոր զրույցի գիծը սահում է ձախից, մինչդեռ այն խամրում է, այլ ոչ թե ամբողջությամբ երևալու:",
"horizon.extras.chat_indicator.tooltip": "Զրույցի գծի ձախ կողմում գտնվող գունավոր գիծը:",
"horizon.extras.wheel_cycles_settings.tooltip": "Ոլորման անիվը կոճակի վրա կարգավորումներում փոխում է իր արժեքը, ինչպես վանիլինը: Անջատված, անիվը միայն ոլորում է ցուցակը:",
"horizon.extras.pack_browser.tooltip": "Ընտրված ռեսուրսների փաթեթները ստանում են Ավելացնել կոճակ, որը որոնում է Modrinth-ը և տեղադրում փաթեթը ձեր փաթեթների թղթապանակում: Անջատված է, խաղը երբեք չի կապվում Մոդրինթի հետ:",
"horizon.extras.splitscreen.tooltip": "Աշխարհում մեկ այլ կարգավորիչ կարող է միանալ որպես նոր խաղացող. սեղմել A-ն, երբ նա հարցնում է, այնուհետև ընտրել պրոֆիլ և որտեղ խաղալ:",
"horizon.splitscreen.setup.monitor": "Մոնիտոր %s",
@@ -464,8 +463,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Թաքնված. յուրաքանչյուր Horizon խաղացող ձեզ տեսնում է առանց ձեր կոսմետիկայի, այդ թվում՝ դուք: Նույն անջատիչը, ինչ գործարկիչի զգեստապահարան:",
"horizon.extras.cosmetics_others": "Այլ խաղացողների կոսմետիկա",
"horizon.extras.cosmetics_others.tooltip": "Ինչ են հագնում մյուս Horizon խաղացողները. ամեն ինչ, միայն նրանց անվան պիտակի ոճերը, կամ ոչինչ: Թաքնված, խաղը երբեք չի հարցնում, թե ինչ են նրանք հագնում:",
"horizon.extras.cosmetics_auras": "Ուրիշների աուրաներն ու արահետները",
"horizon.extras.cosmetics_auras.tooltip": "Աուրաները և արահետները կրում են այլ խաղացողներ: Լռելյայն անջատված է:",
"horizon.extras.cosmetics_hide_fx": "Թաքցնել այլ խաղացողների հետքերն ու աուրաները",
"horizon.extras.cosmetics_hide_fx.tooltip": "Այլ խաղացողների հետքերն ու աուրաները մնում են թաքնված. նրանց մյուս կոսմետիկան դեռ երևում է։ Լռելյայն անջատված է։",
"horizon.extras.cosmetics.shown": "Ցուցադրված է",
"horizon.extras.cosmetics.hidden": "Թաքնված",
"horizon.extras.cosmetics.name_tags": "Միայն անունների պիտակներ"
@@ -300,7 +300,6 @@
"horizon.osk.close": "Tutup",
"horizon.extras.chat_animation.tooltip": "Baris obrolan baru meluncur dari kiri sambil memudar, alih-alih langsung muncul seutuhnya.",
"horizon.extras.chat_indicator.tooltip": "Bilah berwarna di sebelah kiri baris obrolan.",
"horizon.extras.wheel_cycles_settings.tooltip": "Roda gulir di atas tombol pengaturan akan mengubah nilainya, seperti pada versi vanilla. Jika nonaktif, roda gulir hanya menggulir daftar.",
"horizon.extras.splitscreen.tooltip": "Di dalam dunia, kontroler lain dapat bergabung sebagai pemain baru: tekan A saat diminta, lalu pilih profil dan tempat bermain.",
"horizon.splitscreen.setup.monitor_unknown": "sebuah monitor",
"horizon.splitscreen.setup.window": "Jendela %s",
@@ -314,7 +314,6 @@
"horizon.osk.close": "Mechie",
"horizon.extras.chat_animation.tooltip": "Ahịrị nkata ọhụrụ na-esi n'aka ekpe bata ka ọ na-adaba, kama ịpụta zuru oke.",
"horizon.extras.chat_indicator.tooltip": "Ogwe nwere agba dị n'aka ekpe nke ahịrị nkata.",
"horizon.extras.wheel_cycles_settings.tooltip": "Mpempe akwụkwọ mpịakọta n'elu bọtịnụ dị na ntọala na-agbanwe uru ya, dịka ọ dị na vanilla. Gbanyụọ, wiil ahụ na-atụgharị naanị ndepụta ahụ.",
"horizon.extras.pack_browser.tooltip": "Họrọ Ngwungwu akụrụngwa na-enweta bọtịnụ Tinye nke na-achọ Modrinth ma tinye otu mkpọ n'ime folda mkpọ gị. Gbanyụọ, egwuregwu anaghị akpọtụrụ Modrinth.",
"horizon.extras.splitscreen.tooltip": "N'ime ụwa, onye njikwa ọzọ nwere ike isonyere dị ka onye ọkpụkpọ ọhụrụ: pịa A mgbe ọ jụrụ, wee họrọ profaịlụ na ebe ị ga-egwu.",
"horizon.splitscreen.setup.monitor": "Nyochaa %s",
@@ -460,8 +459,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Zoro ezo: Onye ọkpụkpọ ọ bụla Horizon na-ahụ gị na-enweghị ihe ịchọ mma gị, ị gụnyere. Otu mgbanwe dị ka Wardrobe nke onye mbido.",
"horizon.extras.cosmetics_others": "Ngwa mma ndị egwuregwu ndị ọzọ",
"horizon.extras.cosmetics_others.tooltip": "Ihe ndị ọzọ Horizon Player na-eyi: ihe niile, naanị aha mkpado styles, ma ọ bụ ihe ọ bụla. Na nzuzo, egwuregwu anaghị ajụ ihe ha na-eyi.",
"horizon.extras.cosmetics_auras": "Auras na ụzọ ndị ọzọ",
"horizon.extras.cosmetics_auras.tooltip": "Auras na ụzọ ndị egwuregwu ndị ọzọ na-eyi. Gbanyụọ na ndabara.",
"horizon.extras.cosmetics_hide_fx": "Zoo akara ụkwụ na aura ndị egwuregwu ndị ọzọ",
"horizon.extras.cosmetics_hide_fx.tooltip": "Akara ụkwụ na aura ndị egwuregwu ndị ọzọ na-anọgide zoro ezo; ihe ịchọ mma ha ndị ọzọ ka na-egosi. Gbanyụrụ na ndabara.",
"horizon.extras.cosmetics.shown": "Egosiri",
"horizon.extras.cosmetics.hidden": "Zoro ezo",
"horizon.extras.cosmetics.name_tags": "Naanị mkpado aha"
@@ -320,7 +320,6 @@
"horizon.osk.close": "Loka",
"horizon.extras.chat_animation.tooltip": "Ný spjalllína rennur inn frá vinstri á meðan hún dofnar inn, í stað þess að virðast heil.",
"horizon.extras.chat_indicator.tooltip": "Litaða stikan vinstra megin á spjalllínu.",
"horizon.extras.wheel_cycles_settings.tooltip": "Skrunahjólið yfir hnapp í stillingunum breytir gildi þess, eins og í vanillu. Slökkt, hjólið flettir aðeins listann.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs fær hnappinn Bæta við sem leitar í Modrinth og setur upp pakka í pakkamöppuna þína. Slökkt, leikurinn hefur aldrei samband við Modrinth.",
"horizon.extras.splitscreen.tooltip": "Í heimi getur annar stjórnandi tekið þátt sem nýr leikmaður: ýttu á A þegar hann spyr, veldu síðan prófíl og hvar á að spila.",
"horizon.splitscreen.setup.monitor": "Fylgjast með %s",
@@ -465,8 +464,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Falinn: sérhver Horizon leikmaður sér þig án snyrtivörunnar þinnar, þú meðtalin. Sami rofi og fataskápur sjósetjunnar.",
"horizon.extras.cosmetics_others": "Snyrtivörur annarra leikmanna",
"horizon.extras.cosmetics_others.tooltip": "Það sem aðrir Horizon-spilarar klæðast: allt, aðeins nafnmerkisstíllinn eða ekkert. Falinn, leikurinn spyr aldrei hverju þeir klæðist.",
"horizon.extras.cosmetics_auras": "Aurar og slóðir annarra",
"horizon.extras.cosmetics_auras.tooltip": "Auras og slóðir sem aðrir leikmenn klæðast. Slökkt sjálfgefið.",
"horizon.extras.cosmetics_hide_fx": "Fela slóðir og árur annarra leikmanna",
"horizon.extras.cosmetics_hide_fx.tooltip": "Slóðir og árur annarra leikmanna eru faldar; aðrir skrautmunir þeirra sjást áfram. Slökkt sjálfgefið.",
"horizon.extras.cosmetics.shown": "Sýnd",
"horizon.extras.cosmetics.hidden": "Falinn",
"horizon.extras.cosmetics.name_tags": "Eingöngu nafnamerki",
@@ -331,7 +331,6 @@
"horizon.osk.close": "Chiudi",
"horizon.extras.chat_animation.tooltip": "Una nuova linea di chat scorre da sinistra mentre si dissolve, invece di apparire intera.",
"horizon.extras.chat_indicator.tooltip": "La barra colorata a sinistra di una chat line.",
"horizon.extras.wheel_cycles_settings.tooltip": "La rotella di scorrimento sopra un pulsante nelle impostazioni ne modifica il valore, come in Vanilla. Spento, la rotella scorre solo l'elenco.",
"horizon.extras.pack_browser.tooltip": "Seleziona pacchetti di risorse ottiene un pulsante Aggiungi che cerca Modrinth e installa un pacchetto nella cartella dei pacchetti. Spento, il gioco non contatta mai Modrinth.",
"horizon.extras.splitscreen.tooltip": "In un mondo, un altro controller può unirsi come nuovo giocatore: premi A quando richiesto, quindi scegli un profilo e dove giocare.",
"horizon.splitscreen.setup.monitor": "Monitorare %s",
@@ -480,8 +479,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Nascosto: ogni giocatore di Horizon ti vede senza i tuoi cosmetici, incluso te. Lo stesso interruttore dell'armadio del lanciatore.",
"horizon.extras.cosmetics_others": "Cosmetici di altri giocatori",
"horizon.extras.cosmetics_others.tooltip": "Cosa indossano gli altri giocatori di Horizon: tutto, solo lo stile delle etichette con il nome o niente. Nascosto, il gioco non chiede mai cosa indossano.",
"horizon.extras.cosmetics_auras": "Aure e tracce degli altri",
"horizon.extras.cosmetics_auras.tooltip": "Aure e tracce indossate da altri giocatori. Disattivato per impostazione predefinita.",
"horizon.extras.cosmetics_hide_fx": "Nascondi scie e aure degli altri giocatori",
"horizon.extras.cosmetics_hide_fx.tooltip": "Le scie e le aure degli altri giocatori restano nascoste; gli altri loro cosmetici restano visibili. Disattivato per impostazione predefinita.",
"horizon.extras.cosmetics.shown": "Mostrato",
"horizon.extras.cosmetics.hidden": "Nascosto",
"horizon.extras.cosmetics.name_tags": "Solo targhette con nome"
@@ -334,7 +334,6 @@
"horizon.osk.close": "閉じる",
"horizon.extras.chat_animation.tooltip": "新しいチャット ラインは全体として表示されるのではなく、フェードインしながら左側からスライドして表示されます。",
"horizon.extras.chat_indicator.tooltip": "チャットラインの左側にある色付きのバー。",
"horizon.extras.wheel_cycles_settings.tooltip": "バニラと同様に、設定内のボタン上でホイールをスクロールすると、その値が変更されます。オフの場合、ホイールはリストのみをスクロールします。",
"horizon.extras.pack_browser.tooltip": "[リソース パック] を選択すると、Modrinth を検索してパック フォルダーにパックをインストールする [追加] ボタンが表示されます。オフ、ゲームは決してモドリンスに接触しません。",
"horizon.extras.splitscreen.tooltip": "ワールドでは、別のコントローラーが新しいプレーヤーとして参加できます。メッセージが表示されたら A を押し、プロフィールとプレイする場所を選択します。",
"horizon.splitscreen.setup.monitor": "モニター%s",
@@ -487,8 +486,8 @@
"horizon.extras.cosmetics_mine.tooltip": "非表示: すべての Horizon プレイヤーは、あなたも含め、化粧品を着用していないあなたを見ます。ランチャーのワードローブと同じスイッチ。",
"horizon.extras.cosmetics_others": "他のプレイヤーの化粧品",
"horizon.extras.cosmetics_others.tooltip": "他の Horizon プレイヤーが着ているもの: すべて、名札のスタイルのみ、または何も着ていません。隠されていますが、ゲームでは彼らが何を着ているかは尋ねられません。",
"horizon.extras.cosmetics_auras": "他人のオーラと軌跡",
"horizon.extras.cosmetics_auras.tooltip": "他のプレイヤーが身に着けているオーラと軌跡。デフォルトではオフです。",
"horizon.extras.cosmetics_hide_fx": "他のプレイヤーの軌跡とオーラを隠す",
"horizon.extras.cosmetics_hide_fx.tooltip": "他のプレイヤーの軌跡とオーラを非表示にします。それ以外のコスメティックは表示されたままです。デフォルトはオフ。",
"horizon.extras.cosmetics.shown": "示されている",
"horizon.extras.cosmetics.hidden": "隠された",
"horizon.extras.cosmetics.name_tags": "ネームタグのみ"
@@ -324,7 +324,6 @@
"horizon.osk.close": "დახურვა",
"horizon.extras.chat_animation.tooltip": "ჩეთის ახალი ხაზი შემოდის მარცხნიდან, სანამ ის ქრება, ნაცვლად იმისა, რომ მთლიანად გამოჩნდეს.",
"horizon.extras.chat_indicator.tooltip": "ფერადი ზოლი ჩატის ხაზის მარცხნივ.",
"horizon.extras.wheel_cycles_settings.tooltip": "ღილაკზე გადახვევის ბორბალი პარამეტრებში ცვლის მის მნიშვნელობას, როგორც ვანილის შემთხვევაში. გამორთულია, საჭე მხოლოდ გადახვევს სიას.",
"horizon.extras.pack_browser.tooltip": "რესურსების პაკეტების არჩევა მიიღებს დამატებას ღილაკს, რომელიც ეძებს Modrinth-ს და დააინსტალირებს პაკეტს თქვენს პაკეტების საქაღალდეში. გამორთულია, თამაში არასოდეს დაუკავშირდება მოდრინთს.",
"horizon.extras.splitscreen.tooltip": "მსოფლიოში, სხვა კონტროლერს შეუძლია შეუერთდეს ახალ მოთამაშეს: დააჭირე A-ს, როცა ის ითხოვს, შემდეგ აირჩიე პროფილი და სად უნდა ითამაშოს.",
"horizon.splitscreen.setup.monitor": "მონიტორი %s",
@@ -472,8 +471,8 @@
"horizon.extras.cosmetics_mine.tooltip": "დამალული: ყოველი Horizon მოთამაშე გხედავთ თქვენი კოსმეტიკის გარეშე, თქვენ ჩათვლით. იგივე გადამრთველი, როგორც გამშვების გარდერობი.",
"horizon.extras.cosmetics_others": "სხვა მოთამაშეების კოსმეტიკა",
"horizon.extras.cosmetics_others.tooltip": "რას აცვია სხვა Horizon მოთამაშეები: ყველაფერი, მხოლოდ მათი სახელების სტილები, ან არაფერი. დამალული, თამაში არასდროს ეკითხება, რა აცვიათ.",
"horizon.extras.cosmetics_auras": "სხვების აურა და ბილიკები",
"horizon.extras.cosmetics_auras.tooltip": "აურები და ბილიკები ატარებენ სხვა მოთამაშეებს. ნაგულისხმევად გამორთულია.",
"horizon.extras.cosmetics_hide_fx": "სხვა მოთამაშეების კვალისა და აურების დამალვა",
"horizon.extras.cosmetics_hide_fx.tooltip": "სხვა მოთამაშეების კვალი და აურები დამალული რჩება; მათი სხვა კოსმეტიკა კვლავ ჩანს. ნაგულისხმევად გამორთულია.",
"horizon.extras.cosmetics.shown": "ნაჩვენებია",
"horizon.extras.cosmetics.hidden": "დამალული",
"horizon.extras.cosmetics.name_tags": "მხოლოდ სახელების ტეგები"
@@ -325,7 +325,6 @@
"horizon.osk.close": "Жабу",
"horizon.extras.chat_animation.tooltip": "Жаңа сөйлесу сызығы тұтас болып көрінудің орнына, өшіп жатқанда, сол жақтан сырғытады.",
"horizon.extras.chat_indicator.tooltip": "Сөйлесу жолының сол жағындағы түсті жолақ.",
"horizon.extras.wheel_cycles_settings.tooltip": "Параметрлердегі түйменің үстіндегі айналдыру дөңгелегі ванильдегі сияқты оның мәнін өзгертеді. Өшірулі, дөңгелек тек тізімді айналдырады.",
"horizon.extras.pack_browser.tooltip": "Ресурс бумаларын таңдау Modrinth-ті іздейтін және буманы бумалар қалтасына орнататын Қосу түймешігін алады. Өшірулі, ойын ешқашан Модринтпен байланыспайды.",
"horizon.extras.splitscreen.tooltip": "Әлемде басқа контроллер жаңа ойыншы ретінде қосыла алады: ол сұрағанда A түймесін басыңыз, содан кейін профильді және ойнайтын орынды таңдаңыз.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -474,8 +473,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Жасырын: әрбір Horizon ойыншы сізді косметикасыз көреді, сіз қосасыз. Іске қосу құрылғысының гардеробымен бірдей қосқыш.",
"horizon.extras.cosmetics_others": "Басқа ойыншылардың косметикасы",
"horizon.extras.cosmetics_others.tooltip": "Басқа Horizon ойыншылары не киеді: барлығы, тек олардың атау тегінің стильдері немесе ештеңе жоқ. Жасырын, ойын ешқашан не киетінін сұрамайды.",
"horizon.extras.cosmetics_auras": "Басқалардың ауралары мен жолдары",
"horizon.extras.cosmetics_auras.tooltip": "Басқа ойыншылар киетін ауралар мен жолдар. Әдепкі бойынша өшірулі.",
"horizon.extras.cosmetics_hide_fx": "Басқа ойыншылардың іздері мен аураларын жасыру",
"horizon.extras.cosmetics_hide_fx.tooltip": "Басқа ойыншылардың іздері мен ауралары жасырын қалады; олардың басқа косметикасы көрінеді. Әдепкі бойынша өшірулі.",
"horizon.extras.cosmetics.shown": "Көрсетілген",
"horizon.extras.cosmetics.hidden": "Жасырын",
"horizon.extras.cosmetics.name_tags": "Тек атау тегтері"
@@ -331,7 +331,6 @@
"horizon.osk.close": "ಮುಚ್ಚಿ",
"horizon.extras.chat_animation.tooltip": "ಹೊಸ ಚಾಟ್ ಲೈನ್ ಸಂಪೂರ್ಣವಾಗಿ ಕಾಣಿಸಿಕೊಳ್ಳುವ ಬದಲು ಮಸುಕಾಗುವಾಗ ಎಡದಿಂದ ಸ್ಲೈಡ್ ಆಗುತ್ತದೆ.",
"horizon.extras.chat_indicator.tooltip": "ಚಾಟ್ ಲೈನ್‌ನ ಎಡಭಾಗದಲ್ಲಿ ಬಣ್ಣದ ಪಟ್ಟಿ.",
"horizon.extras.wheel_cycles_settings.tooltip": "ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿನ ಬಟನ್‌ನ ಮೇಲಿನ ಸ್ಕ್ರಾಲ್ ಚಕ್ರವು ವೆನಿಲ್ಲಾದಂತೆ ಅದರ ಮೌಲ್ಯವನ್ನು ಬದಲಾಯಿಸುತ್ತದೆ. ಆಫ್, ಚಕ್ರವು ಪಟ್ಟಿಯನ್ನು ಮಾತ್ರ ಸ್ಕ್ರಾಲ್ ಮಾಡುತ್ತದೆ.",
"horizon.extras.pack_browser.tooltip": "ಸಂಪನ್ಮೂಲ ಪ್ಯಾಕ್‌ಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ ಆಡ್ ಬಟನ್ ಅನ್ನು ಪಡೆಯುತ್ತದೆ ಅದು Modrinth ಅನ್ನು ಹುಡುಕುತ್ತದೆ ಮತ್ತು ನಿಮ್ಮ ಪ್ಯಾಕ್‌ಗಳ ಫೋಲ್ಡರ್‌ಗೆ ಪ್ಯಾಕ್ ಅನ್ನು ಸ್ಥಾಪಿಸುತ್ತದೆ. ಆಫ್, ಆಟವು Modrinth ಅನ್ನು ಎಂದಿಗೂ ಸಂಪರ್ಕಿಸುವುದಿಲ್ಲ.",
"horizon.extras.splitscreen.tooltip": "ಜಗತ್ತಿನಲ್ಲಿ, ಮತ್ತೊಂದು ನಿಯಂತ್ರಕವು ಹೊಸ ಆಟಗಾರನಾಗಿ ಸೇರಿಕೊಳ್ಳಬಹುದು: ಅದು ಕೇಳಿದಾಗ A ಅನ್ನು ಒತ್ತಿ, ನಂತರ ಪ್ರೊಫೈಲ್ ಅನ್ನು ಮತ್ತು ಎಲ್ಲಿ ಪ್ಲೇ ಮಾಡಬೇಕೆಂದು ಆಯ್ಕೆಮಾಡಿ.",
"horizon.splitscreen.setup.monitor": "ಮಾನಿಟರ್ %s",
@@ -481,8 +480,8 @@
"horizon.extras.cosmetics_mine.tooltip": "ಮರೆಮಾಡಲಾಗಿದೆ: ಪ್ರತಿ Horizon ಪ್ಲೇಯರ್ ನಿಮ್ಮ ಸೌಂದರ್ಯವರ್ಧಕಗಳಿಲ್ಲದೆ ನಿಮ್ಮನ್ನು ನೋಡುತ್ತಾರೆ, ನೀವು ಸೇರಿಸಿದ್ದೀರಿ. ಲಾಂಚರ್‌ನ ವಾರ್ಡ್‌ರೋಬ್‌ನಂತೆಯೇ ಅದೇ ಸ್ವಿಚ್.",
"horizon.extras.cosmetics_others": "ಇತರ ಆಟಗಾರರ ಸೌಂದರ್ಯವರ್ಧಕಗಳು",
"horizon.extras.cosmetics_others.tooltip": "ಇತರ Horizon ಆಟಗಾರರು ಏನು ಧರಿಸುತ್ತಾರೆ: ಎಲ್ಲವೂ, ಅವರ ಹೆಸರಿನ ಟ್ಯಾಗ್ ಶೈಲಿಗಳು ಅಥವಾ ಏನೂ ಇಲ್ಲ. ಮರೆಮಾಡಲಾಗಿದೆ, ಆಟವು ಅವರು ಏನು ಧರಿಸುತ್ತಾರೆ ಎಂದು ಎಂದಿಗೂ ಕೇಳುವುದಿಲ್ಲ.",
"horizon.extras.cosmetics_auras": "ಇತರರ ಸೆಳವು ಮತ್ತು ಹಾದಿಗಳು",
"horizon.extras.cosmetics_auras.tooltip": "ಔರಾಸ್ ಮತ್ತು ಟ್ರೇಲ್ಸ್ ಇತರ ಆಟಗಾರರು ಧರಿಸುತ್ತಾರೆ. ಡೀಫಾಲ್ಟ್ ಆಗಿ ಆಫ್.",
"horizon.extras.cosmetics_hide_fx": "ಇತರ ಆಟಗಾರರ ಹಾದಿಗಳು ಮತ್ತು ಪ್ರಭಾವಳಿಗಳನ್ನು ಮರೆಮಾಡಿ",
"horizon.extras.cosmetics_hide_fx.tooltip": "ಇತರ ಆಟಗಾರರ ಹಾದಿಗಳು ಮತ್ತು ಪ್ರಭಾವಳಿಗಳು ಮರೆಯಾಗಿರುತ್ತವೆ; ಅವರ ಇತರ ಅಲಂಕಾರಗಳು ಕಾಣುತ್ತವೆ. ಪೂರ್ವನಿಯೋಜಿತವಾಗಿ ಆಫ್.",
"horizon.extras.cosmetics.shown": "ತೋರಿಸಲಾಗಿದೆ",
"horizon.extras.cosmetics.hidden": "ಮರೆಮಾಡಲಾಗಿದೆ",
"horizon.extras.cosmetics.name_tags": "ಹೆಸರು ಟ್ಯಾಗ್‌ಗಳು ಮಾತ್ರ"
@@ -308,7 +308,6 @@
"horizon.osk.close": "닫기",
"horizon.extras.chat_animation.tooltip": "새 채팅 줄이 통째로 나타나는 대신 왼쪽에서 미끄러지듯 나타나며 서서히 페이드 인됩니다.",
"horizon.extras.chat_indicator.tooltip": "채팅 줄 왼쪽에 있는 색상 바입니다.",
"horizon.extras.wheel_cycles_settings.tooltip": "설정에서 버튼 위에 스크롤 휠을 올리면 바닐라처럼 값이 변경됩니다. 끄면 휠이 목록 스크롤만 수행합니다.",
"horizon.extras.splitscreen.tooltip": "월드 내에서 다른 컨트롤러로 새 플레이어로 참여할 수 있습니다: 메시지가 나타날 때 A를 누르고 프로필과 플레이할 위치를 선택하세요.",
"horizon.splitscreen.setup.monitor": "모니터 %s",
"horizon.splitscreen.setup.monitor_unknown": "모니터",
@@ -301,7 +301,6 @@
"horizon.osk.close": "Жабуу",
"horizon.extras.chat_animation.tooltip": "Жаңы чат сызыгы сол жактан жылдырып, ал толугу менен көрүнбөй, өчүп калат.",
"horizon.extras.chat_indicator.tooltip": "Чат сызыгынын сол жагындагы түстүү тилке.",
"horizon.extras.wheel_cycles_settings.tooltip": "Жөндөөлөрдөгү баскычтын үстүнөн жылдыргыч дөңгөлөк ванильдегидей анын маанисин өзгөртөт. Өчүк, дөңгөлөк тизмени гана жылдырат.",
"horizon.extras.pack_browser.tooltip": "Ресурстук топтомдорду тандоо Modrinth издеген Кошуу баскычын алат жана пакеттер папкаңызга пакетти орнотот. Өчүк, оюн эч качан Modrinth менен байланышпайт.",
"horizon.extras.splitscreen.tooltip": "Дүйнөдө башка контроллер жаңы оюнчу катары кошула алат: ал сураганда A баскычын басыңыз, андан кийин профилди жана кайда ойноону тандаңыз.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -448,8 +447,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Жашырылган: ар бир Horizon оюнчу сизди косметикасыз көрөт. Ишке киргизгичтин гардеробу сыяктуу эле которгуч.",
"horizon.extras.cosmetics_others": "Башка оюнчулардын косметикасы",
"horizon.extras.cosmetics_others.tooltip": "Башка Horizon оюнчулары кандай кийимдерди кийишет: бардыгы, алардын аты тегинин стили гана же эч нерсе. Жашыруун, оюн эч качан алар эмне кийип жүргөнүн сурабайт.",
"horizon.extras.cosmetics_auras": "Башкалардын ауралары жана жолдору",
"horizon.extras.cosmetics_auras.tooltip": "Ауралар жана башка оюнчулар кийген жолдор. Демейки боюнча өчүк.",
"horizon.extras.cosmetics_hide_fx": "Башка оюнчулардын издерин жана ауралары жашыруу",
"horizon.extras.cosmetics_hide_fx.tooltip": "Башка оюнчулардын издери жана ауралары жашыруун калат; алардын башка косметикасы көрүнөт. Демейки боюнча өчүк.",
"horizon.extras.cosmetics.shown": "Көрсөтүлдү",
"horizon.extras.cosmetics.hidden": "Жашырылган",
"horizon.extras.cosmetics.name_tags": "Аталышы гана"
@@ -264,7 +264,6 @@
"horizon.osk.symbols": "Symbola",
"horizon.extras.chat_animation.tooltip": "Novum chat linea labitur in a sinistra, dum recedit in, non apparere totum.",
"horizon.extras.chat_indicator.tooltip": "Talea colorata ad sinistram lineae chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "Volubilis rota super bullam in uncinis mutat valorem suum, ut in vanilla. Abi, rota tantum volumina ponit.",
"horizon.extras.pack_browser.tooltip": "Lego Resource Sarcinis tesseram additam accipit quae 0⟧ scrutatur et sarcinam in folder sarcinis condit. Abi, ludum numquam contactus Modrinth.",
"horizon.extras.splitscreen.tooltip": "In mundo, alius moderatoris ut novus lusor iungere potest: instare A cum quaerit, deinde figuram carpe et ubi ludere.",
"horizon.splitscreen.setup.monitor": "Monitor%s",
@@ -399,8 +398,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Occultus: omnisHorizonlusor te sine tuis fucis videt, te comprehendit. Idem transitum cum vestiario launcher.",
"horizon.extras.cosmetics_others": "Alii histriones medicamentis",
"horizon.extras.cosmetics_others.tooltip": "Quid aliud?Horizonhistriones gerunt: omnia, solum nomen tag stili, vel nihil. Occultus, ludus numquam quaerit quod gerunt.",
"horizon.extras.cosmetics_auras": "Aliorum auras et calles",
"horizon.extras.cosmetics_auras.tooltip": "Auras et calles alii lusores gerunt. Off per defaltam.",
"horizon.extras.cosmetics_hide_fx": "Vestigia et auras aliorum lusorum celare",
"horizon.extras.cosmetics_hide_fx.tooltip": "Vestigia et aurae aliorum lusorum celata manent; cetera ornamenta eorum adhuc videntur. Ex defalta exstinctum.",
"horizon.extras.cosmetics.shown": "Ostensum",
"horizon.extras.cosmetics.hidden": "Occultus",
"horizon.extras.cosmetics.name_tags": "Nomen tags tantum"
@@ -293,7 +293,6 @@
"horizon.osk.close": "Zoumaachen",
"horizon.extras.chat_animation.tooltip": "Eng nei Chat-Linn rutscht vu lénks eran, während se verschwënnt, amplaz ganz ze erschéngen.",
"horizon.extras.chat_indicator.tooltip": "Déi faarweg Bar op der lénker Säit vun enger Chat Linn.",
"horizon.extras.wheel_cycles_settings.tooltip": "De Scrollrad iwwer e Knäppchen an den Astellungen ännert säi Wäert, wéi an Vanille. Off, d'Rad scrollt nëmmen d'Lëscht.",
"horizon.extras.pack_browser.tooltip": "Wielt Ressource Packs kritt en Add Button deen Modrinth sicht an e Pack an Äre Packs Dossier installéiert. Off, d'Spill kontaktéiert ni Modrinth.",
"horizon.extras.splitscreen.tooltip": "An enger Welt kann en anere Controller als neie Spiller matmaachen: dréckt A wann et freet, wielt dann e Profil a wou Dir spillt.",
"horizon.splitscreen.setup.monitor_unknown": "engem Monitor",
@@ -435,8 +434,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Verstoppt: all Horizon Spiller gesäit Iech ouni Är Kosmetik, Dir abegraff. Deeselwechte Schalter wéi de Launcher Kleederschaf.",
"horizon.extras.cosmetics_others": "Aner Spiller Kosmetik",
"horizon.extras.cosmetics_others.tooltip": "Wat aner Horizon Spiller droen: alles, nëmmen hir Numm Tag Stiler, oder näischt. Verstoppt, d'Spill freet ni wat se droen.",
"horizon.extras.cosmetics_auras": "Anerer hir Auraen an Trëppelweeër",
"horizon.extras.cosmetics_auras.tooltip": "Auras an Trëppelweeër aner Spiller zouzedrécken. Off par défaut.",
"horizon.extras.cosmetics_hide_fx": "Spueren an Auraen vun anere Spiller verstoppen",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spueren an Auraen vun anere Spiller bleiwe verstoppt; hir aner Kosmetik ass ëmmer nach ze gesinn. Standardméisseg aus.",
"horizon.extras.cosmetics.shown": "gewisen",
"horizon.extras.cosmetics.hidden": "Verstoppt",
"horizon.extras.cosmetics.name_tags": "Numm Tags nëmmen"
@@ -291,7 +291,6 @@
"horizon.osk.close": "Sluite",
"horizon.extras.chat_animation.tooltip": "‘n Nuie chatlijn glijdt vaanaaf links in onderwieles dat ‘t in vervaag, in plaats vaan gans te versjiene.",
"horizon.extras.chat_indicator.tooltip": "De gekleurde balk links vaan ‘n chatlijn.",
"horizon.extras.wheel_cycles_settings.tooltip": "‘t Scrollwiel euver ‘n knop in de insjtellinge verandert de waarde, wie in vanille. Oet, ‘t wiel scrollt allein de lies.",
"horizon.extras.pack_browser.tooltip": "Selecteer Resource Packs kriegste ‘n Add-knop dee Modrinth zeuk en ‘n pack in de packs-map installeert. Oet, ‘t spel contacteert noets Modrinth.",
"horizon.extras.splitscreen.tooltip": "In ‘n wereld kin ‘n aandere controller bijtrejje es ‘n nuie speuler: druk op A es ‘t vraog, kies daonao ‘n profiel en boe te speule.",
"horizon.splitscreen.setup.monitor_unknown": "un monitor",
@@ -434,8 +433,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Verborge: eder Horizon speuler ziet dich zoonder dien cosmetica, inclusief dich. Dezelfde schakelaar es de garderobe vaan de launcher.",
"horizon.extras.cosmetics_others": "Cosmetica vaan aandere speulers",
"horizon.extras.cosmetics_others.tooltip": "Wat aandere Horizon speulers drage: alles, allein hun stijl vaan naamlabel, of niks. Verborge, vraog ‘t spel noets wat ze drage.",
"horizon.extras.cosmetics_auras": "Auras en spore vaan aandere",
"horizon.extras.cosmetics_auras.tooltip": "Auras en spore die aandere speulers drage. Standaard oet.",
"horizon.extras.cosmetics_hide_fx": "Spore en aura's van anger spelers verberge",
"horizon.extras.cosmetics_hide_fx.tooltip": "Spore en aura's van anger spelers blieve verborge; hun anger cosmetica blieve zichbaar. Standaard oet.",
"horizon.extras.cosmetics.shown": "Getoend",
"horizon.extras.cosmetics.hidden": "Verborge",
"horizon.extras.cosmetics.name_tags": "Allein naamtags"
@@ -321,7 +321,6 @@
"horizon.osk.close": "Serra",
"horizon.extras.chat_animation.tooltip": "Una nœuva linea de chat scivola denter de sinistra menter la sbiadiss, inveci de cumparir intera.",
"horizon.extras.chat_indicator.tooltip": "La barra culurada a sinistra de una linea de chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "La rœuda de scorriment sü un buttun in di impostaziun cambia el sò valur, cuma in de la vaniglia. Disattiva, la rœuda scorre sultant la lista.",
"horizon.extras.pack_browser.tooltip": "Seleziuna Pachett de risurs g’he un buttun Aggiungi che cerca Modrinth e instala un pachett in de la cartella di pachett. Off, el giœugh se cuntat mai Modrinth.",
"horizon.extras.splitscreen.tooltip": "En un mund, un olter cuntroladur pœu entrà denter cuma nœuf giucadur: premet A quand che dumanda, e dopu seleziuna un profil e indué giugar.",
"horizon.splitscreen.setup.monitor_unknown": "un monitor",
@@ -474,5 +473,5 @@
"horizon.extras.cosmetics_mine.tooltip": "Ascüs: tüt i giucadur Horizon te veden senza i tò cosmetich, anca tì. L’istess interruttur del guardaroba del lanciador.",
"horizon.extras.cosmetics_others": "Cosmetich di olter giucadur",
"horizon.extras.cosmetics_others.tooltip": "Che ròbb che purtan sü i olter giucadur Horizon: tüt, sultant i luur stil de etichett o nient. Ascüs, el giœugh dumanda mai cus’è che g’han sü.",
"horizon.extras.cosmetics_auras.tooltip": "Auras and trails other players wear. Disattiva per predefiniziun."
"horizon.extras.cosmetics_hide_fx.tooltip": "I strasc e i aur di olter giugadur resten scondu; i sò olter cosmetegh se veden anmò. Smorzaa de default."
}
@@ -320,7 +320,6 @@
"horizon.osk.close": "ປິດ",
"horizon.extras.chat_animation.tooltip": "ແຖວສົນທະນາໃໝ່ເລື່ອນເຂົ້າມາຈາກຊ້າຍໃນຂະນະທີ່ມັນຈາງລົງ, ແທນທີ່ຈະປາກົດທັງໝົດ.",
"horizon.extras.chat_indicator.tooltip": "ແຖບສີຢູ່ເບື້ອງຊ້າຍຂອງສາຍສົນທະນາ.",
"horizon.extras.wheel_cycles_settings.tooltip": "ລໍ້ເລື່ອນຜ່ານປຸ່ມໃດນຶ່ງໃນການຕັ້ງຄ່າຈະປ່ຽນຄ່າຂອງມັນ, ຄືກັບໃນ vanilla. ປິດ, ລໍ້ພຽງແຕ່ເລື່ອນບັນຊີລາຍຊື່.",
"horizon.extras.pack_browser.tooltip": "ເລືອກ Resource Packs ໄດ້ຮັບປຸ່ມ Add ທີ່ຊອກຫາ Modrinth ແລະຕິດຕັ້ງຊຸດໃສ່ໃນໂຟນເດີ packs ຂອງທ່ານ. ປິດ, ເກມບໍ່ເຄີຍຕິດຕໍ່ Modrinth.",
"horizon.extras.splitscreen.tooltip": "ໃນໂລກ, ຜູ້ຄວບຄຸມອື່ນສາມາດເຂົ້າຮ່ວມເປັນຜູ້ນໃຫມ່: ກົດ A ເມື່ອມັນຖາມ, ຫຼັງຈາກນັ້ນເລືອກໂປຣໄຟລ໌ແລະບ່ອນທີ່ຈະຫຼິ້ນ.",
"horizon.splitscreen.setup.monitor": "ຈໍພາບ %s",
@@ -464,8 +463,8 @@
"horizon.extras.cosmetics_mine.tooltip": "ເຊື່ອງໄວ້: ຜູ້ນ Horizon ທຸກຄົນເຫັນທ່ານໂດຍບໍ່ມີເຄື່ອງສໍາອາງຂອງທ່ານ, ທ່ານລວມ. ສະຫຼັບດຽວກັນກັບຕູ້ເສື້ອຜ້າຂອງຕົວເປີດ.",
"horizon.extras.cosmetics_others": "ເຄື່ອງສໍາອາງຂອງຜູ້ນອື່ນໆ",
"horizon.extras.cosmetics_others.tooltip": "ສິ່ງທີ່ຜູ້ຫຼິ້ນ Horizon ຄົນອື່ນໆໃສ່: ທຸກຢ່າງ, ພຽງແຕ່ຮູບແບບປ້າຍຊື່ຂອງເຂົາເຈົ້າ, ຫຼືບໍ່ມີຫຍັງ. ເຊື່ອງໄວ້, ເກມບໍ່ເຄີຍຖາມວ່າພວກເຂົາໃສ່ຫຍັງ.",
"horizon.extras.cosmetics_auras": "ອໍຣາ ແລະເສັ້ນທາງຂອງຄົນອື່ນ",
"horizon.extras.cosmetics_auras.tooltip": "Aura ແລະ trails ຜູ້ນອື່ນໆໃສ່. ປິດຕາມຄ່າເລີ່ມຕົ້ນ.",
"horizon.extras.cosmetics_hide_fx": "ເຊື່ອງຮອຍ ແລະ ອໍຣາຂອງຜູ້ຫຼິ້ນອື່ນ",
"horizon.extras.cosmetics_hide_fx.tooltip": "ຮອຍ ແລະ ອໍຣາຂອງຜູ້ຫຼິ້ນອື່ນຖືກເຊື່ອງໄວ້; ເຄື່ອງປະດັບອື່ນຂອງເຂົາເຈົ້າຍັງເຫັນຢູ່. ປິດໄວ້ຕາມຄ່າເລີ່ມຕົ້ນ.",
"horizon.extras.cosmetics.shown": "ສະແດງໃຫ້ເຫັນ",
"horizon.extras.cosmetics.hidden": "ເຊື່ອງໄວ້",
"horizon.extras.cosmetics.name_tags": "ປ້າຍຊື່ເທົ່ານັ້ນ",
@@ -323,7 +323,6 @@
"horizon.osk.close": "Uždaryti",
"horizon.extras.chat_animation.tooltip": "Nauja pokalbio linija slenka iš kairės, kol ji išnyksta, o ne atrodo visa.",
"horizon.extras.chat_indicator.tooltip": "Spalvota juosta pokalbio linijos kairėje.",
"horizon.extras.wheel_cycles_settings.tooltip": "Slinkties ratukas virš mygtuko nustatymuose keičia jo reikšmę, kaip ir vanilės atveju. Išjungtas, ratas tik slenka sąrašu.",
"horizon.extras.pack_browser.tooltip": "Pasirinkite Išteklių paketai gauna mygtuką Pridėti, kuris ieško Modrinth ir įdiegia paketą į jūsų paketų aplanką. Išjungtas, žaidimas niekada nesusisiekia su Modrinth.",
"horizon.extras.splitscreen.tooltip": "Pasaulyje kitas valdiklis gali prisijungti kaip naujas žaidėjas: paspauskite A, kai jis paprašys, tada pasirinkite profilį ir kur žaisti.",
"horizon.splitscreen.setup.monitor": "Monitorius %s",
@@ -467,8 +466,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Paslėpta: kiekvienas „Horizon“ žaidėjas mato jus be jūsų kosmetikos, įskaitant jus. Tas pats jungiklis kaip ir paleidimo priemonės drabužių spinta.",
"horizon.extras.cosmetics_others": "Kitų žaidėjų kosmetika",
"horizon.extras.cosmetics_others.tooltip": "Ką dėvi kiti „Horizon“ žaidėjai: viskas, tik jų vardų etiketės arba nieko. Paslėptas, žaidimas niekada neklausia, ką jie dėvi.",
"horizon.extras.cosmetics_auras": "Kitų auros ir takai",
"horizon.extras.cosmetics_auras.tooltip": "Auros ir takai, kuriuos nešioja kiti žaidėjai. Išjungta pagal numatytuosius nustatymus.",
"horizon.extras.cosmetics_hide_fx": "Slėpti kitų žaidėjų pėdsakus ir auras",
"horizon.extras.cosmetics_hide_fx.tooltip": "Kitų žaidėjų pėdsakai ir auros lieka paslėpti; kita jų kosmetika vis dar matoma. Numatytai išjungta.",
"horizon.extras.cosmetics.shown": "Parodyta",
"horizon.extras.cosmetics.hidden": "Paslėpta",
"horizon.extras.cosmetics.name_tags": "Tik vardų žymos"
@@ -313,7 +313,6 @@
"horizon.osk.close": "Aizvērt",
"horizon.extras.chat_animation.tooltip": "Jauna tērzēšanas līnija ieslīd no kreisās puses, kamēr tā pazūd, nevis tiek rādīta vesela.",
"horizon.extras.chat_indicator.tooltip": "Krāsaina josla pa kreisi no tērzēšanas līnijas.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ritināšanas ritenītis virs pogas iestatījumos maina tās vērtību, tāpat kā vaniļas gadījumā. Izslēgts, ritenis tikai ritina sarakstu.",
"horizon.extras.pack_browser.tooltip": "Atlasīt Resursu pakotnes saņem pogu Pievienot, kas meklē Modrinth un instalē pakotni jūsu pakotņu mapē. Izslēgts, spēle nekad nesazinās ar Modrinth.",
"horizon.extras.splitscreen.tooltip": "Pasaulē cits kontrolieris var pievienoties kā jauns spēlētājs: nospiediet A, kad tas jautā, pēc tam izvēlieties profilu un vietu, kur spēlēt.",
"horizon.splitscreen.setup.monitor": "Monitors %s",
@@ -456,8 +455,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Slēpts: katrs Horizon spēlētājs redz jūs bez jūsu kosmētikas, ieskaitot jūs. Tas pats slēdzis kā palaišanas ierīces Garderobe.",
"horizon.extras.cosmetics_others": "Pārējo spēlētāju kosmētika",
"horizon.extras.cosmetics_others.tooltip": "Ko valkā citi Horizon spēlētāji: viss, tikai viņu vārda birku stili vai nekas. Slēpts, spēle nekad nejautā, ko viņi valkā.",
"horizon.extras.cosmetics_auras": "Citu auras un takas",
"horizon.extras.cosmetics_auras.tooltip": "Auras un takas, ko valkā citi spēlētāji. Pēc noklusējuma izslēgts.",
"horizon.extras.cosmetics_hide_fx": "Slēpt citu spēlētāju pēdas un auras",
"horizon.extras.cosmetics_hide_fx.tooltip": "Citu spēlētāju pēdas un auras paliek paslēptas; viņu pārējā kosmētika joprojām redzama. Pēc noklusējuma izslēgts.",
"horizon.extras.cosmetics.shown": "Parādīts",
"horizon.extras.cosmetics.hidden": "Slēpts",
"horizon.extras.cosmetics.name_tags": "Tikai vārdu birkas"
@@ -330,7 +330,6 @@
"horizon.osk.close": "Затвори",
"horizon.extras.chat_animation.tooltip": "Новата линија за разговор се лизга од лево додека бледне, наместо да се појавува целина.",
"horizon.extras.chat_indicator.tooltip": "Лентата во боја лево од линијата за разговор.",
"horizon.extras.wheel_cycles_settings.tooltip": "Тркалото за лизгање над копче во поставките ја менува својата вредност, како кај ванилата. Исклучено, тркалото само ја прелистува листата.",
"horizon.extras.pack_browser.tooltip": "Изборот на пакети со ресурси добива копче „Додај“ што пребарува на Modrinth и инсталира пакет во папката со пакети. Кога е исклучено, играта никогаш не контактира со Modrinth.",
"horizon.extras.splitscreen.tooltip": "Во светот, друг контролер може да се приклучи како нов играч: притиснете A кога ќе побара, потоа изберете профил и каде да играте.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -480,8 +479,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Скриено: секој играч Horizon ве гледа без вашата козметика, вклучително и вие. Истиот прекинувач како и гардеробата на фрлачот.",
"horizon.extras.cosmetics_others": "Козметика за други играчи",
"horizon.extras.cosmetics_others.tooltip": "Што носат другите Horizon играчи: сè, само стилови на нивните ознаки или ништо. Скриено, играта никогаш не прашува што носат.",
"horizon.extras.cosmetics_auras": "Аури и патеки на другите",
"horizon.extras.cosmetics_auras.tooltip": "Аури и патеки што ги носат другите играчи. Исклучено стандардно.",
"horizon.extras.cosmetics_hide_fx": "Скриј ги трагите и аурите на другите играчи",
"horizon.extras.cosmetics_hide_fx.tooltip": "Трагите и аурите на другите играчи остануваат скриени; нивната друга козметика и понатаму се гледа. Стандардно исклучено.",
"horizon.extras.cosmetics.shown": "Прикажани",
"horizon.extras.cosmetics.hidden": "Скриени",
"horizon.extras.cosmetics.name_tags": "Само ознаки со имиња"
@@ -327,7 +327,6 @@
"horizon.osk.close": "Хаах",
"horizon.extras.chat_animation.tooltip": "Шинэ чатын шугам нь бүхэлдээ харагдахын оронд бүдгэрч байх хооронд зүүн талаас гулсуулна.",
"horizon.extras.chat_indicator.tooltip": "Чатын шугамын зүүн талд байгаа өнгөт мөр.",
"horizon.extras.wheel_cycles_settings.tooltip": "Тохиргооны товчлуур дээр гүйлгэх дугуй нь ванилийн адил утгыг өөрчилдөг. Унтраах үед дугуй нь зөвхөн жагсаалтыг гүйлгэдэг.",
"horizon.extras.pack_browser.tooltip": "\"Нөөцийн багцуудыг сонгох\" нь Modrinth-ээс хайлт хийж, багцыг таны багц хавтсанд суулгадаг Нэмэх товчийг авна. Унтраах, тоглоом хэзээ ч Модринттой холбоо барихгүй.",
"horizon.extras.splitscreen.tooltip": "Дэлхий дээр өөр хянагч шинэ тоглогчоор нэгдэж болно: асуухад A товчийг дарж, профайл болон хаана тоглохоо сонго.",
"horizon.splitscreen.setup.monitor": "Монитор %s",
@@ -475,8 +474,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Далд: Horizon тоглогч бүр таныг гоо сайхны бүтээгдэхүүнгүйгээр хардаг. Эхлүүлэгчийн хувцасны шүүгээтэй ижил унтраалга.",
"horizon.extras.cosmetics_others": "Бусад тоглогчдын гоо сайхны бүтээгдэхүүн",
"horizon.extras.cosmetics_others.tooltip": "Бусад Horizon тоглогчид юу өмсдөг вэ: бүх зүйл, зөвхөн нэрийн шошго, эсвэл юу ч биш. Нуугдсан, тоглоом хэзээ ч юу өмсдөгийг асуудаггүй.",
"horizon.extras.cosmetics_auras": "Бусдын аура, замууд",
"horizon.extras.cosmetics_auras.tooltip": "Аура болон бусад тоглогчдын өмсдөг замууд. Өгөгдмөлөөр унтраа.",
"horizon.extras.cosmetics_hide_fx": "Бусад тоглогчийн мөр ба аурыг нуух",
"horizon.extras.cosmetics_hide_fx.tooltip": "Бусад тоглогчийн мөр ба аур нуугдсан хэвээр байна; тэдний бусад гоёл харагдсаар байна. Анхдагчаар унтраалттай.",
"horizon.extras.cosmetics.shown": "Үзүүлсэн",
"horizon.extras.cosmetics.hidden": "Нуугдсан",
"horizon.extras.cosmetics.name_tags": "Зөвхөн нэрийн шошго"
@@ -315,7 +315,6 @@
"horizon.osk.close": "tutup",
"horizon.extras.chat_animation.tooltip": "Baris sembang baharu meluncur masuk dari kiri semasa ia memudar, bukannya kelihatan keseluruhan.",
"horizon.extras.chat_indicator.tooltip": "Bar berwarna di sebelah kiri baris sembang.",
"horizon.extras.wheel_cycles_settings.tooltip": "Roda tatal di atas butang dalam tetapan menukar nilainya, seperti dalam vanila. Mati, roda hanya menatal senarai.",
"horizon.extras.pack_browser.tooltip": "Pilih Pek Sumber mendapat butang Tambah yang mencari Modrinth dan memasang pek ke dalam folder pek anda. Padam, permainan tidak pernah menghubungi Modrinth.",
"horizon.extras.splitscreen.tooltip": "Dalam dunia, pengawal lain boleh menyertai sebagai pemain baharu: tekan A apabila ia bertanya, kemudian pilih profil dan tempat untuk bermain.",
"horizon.splitscreen.setup.monitor": "Pantau %s",
@@ -460,8 +459,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Tersembunyi: setiap pemain Horizon melihat anda tanpa kosmetik anda, termasuk anda. Suis yang sama seperti Almari Pakaian pelancar.",
"horizon.extras.cosmetics_others": "Kosmetik pemain lain",
"horizon.extras.cosmetics_others.tooltip": "Perkara yang dipakai oleh pemain Horizon lain: segala-galanya, hanya gaya tanda nama mereka, atau tiada apa-apa. Tersembunyi, permainan tidak pernah bertanya apa yang mereka pakai.",
"horizon.extras.cosmetics_auras": "Aura dan jejak orang lain",
"horizon.extras.cosmetics_auras.tooltip": "Aura dan jejak yang dipakai pemain lain. Mati secara lalai.",
"horizon.extras.cosmetics_hide_fx": "Sembunyikan jejak dan aura pemain lain",
"horizon.extras.cosmetics_hide_fx.tooltip": "Jejak dan aura pemain lain kekal tersembunyi; kosmetik mereka yang lain masih kelihatan. Mati secara lalai.",
"horizon.extras.cosmetics.shown": "Ditunjukkan",
"horizon.extras.cosmetics.hidden": "Tersembunyi",
"horizon.extras.cosmetics.name_tags": "Tanda nama sahaja"
@@ -293,7 +293,6 @@
"horizon.osk.close": "Agħlaq",
"horizon.extras.chat_animation.tooltip": "Linja ta' chat ġdida tiżżerżaq 'il ġewwa mix-xellug waqt li tgħib, minflok tidher sħiħa.",
"horizon.extras.chat_indicator.tooltip": "Il-bar ikkulurit fuq ix-xellug ta 'chat line.",
"horizon.extras.wheel_cycles_settings.tooltip": "Ir-rota tal-iscroll fuq buttuna fis-settings tibdel il-valur tagħha, bħal fil-vanilla. Mitfija, ir-rota tiskrollja biss il-lista.",
"horizon.extras.pack_browser.tooltip": "Agħżel Pakketti tar-Riżorsi tikseb buttuna Żid li tfittex Modrinth u tinstalla pakkett fil-folder tal-pakketti tiegħek. Mitfija, il-logħba qatt ma tikkuntattja Modrinth.",
"horizon.extras.splitscreen.tooltip": "F'dinja, kontrollur ieħor jista' jingħaqad bħala plejer ġdid: agħfas A meta jitlob, imbagħad agħżel profil u fejn tilgħab.",
"horizon.splitscreen.setup.monitor_unknown": "monitor",
@@ -432,8 +431,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Moħbija: kull plejer Horizon jarak mingħajr il-kożmetiċi tiegħek, int inkluż. L-istess swiċċ bħall-Gwardarobba tal-lanċjar.",
"horizon.extras.cosmetics_others": "Kosmetiċi ta' plejers oħra",
"horizon.extras.cosmetics_others.tooltip": "X'jilbsu plejers oħra ta' Horizon: kollox, l-istili tat-tag tal-isem tagħhom biss, jew xejn. Moħbija, il-logħba qatt ma tistaqsi dak li jilbsu.",
"horizon.extras.cosmetics_auras": "Awra u traċċi ta’ ħaddieħor",
"horizon.extras.cosmetics_auras.tooltip": "Auras u trails plejers oħra jilbsu. Mitfi awtomatikament.",
"horizon.extras.cosmetics_hide_fx": "Aħbi t-traċċi u l-awri ta' plejers oħra",
"horizon.extras.cosmetics_hide_fx.tooltip": "It-traċċi u l-awri ta' plejers oħra jibqgħu moħbija; il-kożmetiċi l-oħra tagħhom jibqgħu jidhru. Mitfi b'mod awtomatiku.",
"horizon.extras.cosmetics.shown": "murija",
"horizon.extras.cosmetics.hidden": "Moħbija",
"horizon.extras.cosmetics.name_tags": "Tikketti tal-isem biss",
@@ -322,7 +322,6 @@
"horizon.osk.close": "Sluiten",
"horizon.extras.chat_animation.tooltip": "Een nieuwe chatlijn schuift van links naar binnen terwijl deze invaagt, in plaats van geheel te lijken.",
"horizon.extras.chat_indicator.tooltip": "De gekleurde balk links van een chatlijn.",
"horizon.extras.wheel_cycles_settings.tooltip": "Het scrollwiel boven een knop in de instellingen verandert de waarde, zoals in vanille. Uit: het wiel scrollt alleen door de lijst.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs krijgt een knop Toevoegen die Modrinth doorzoekt en een pakket in uw packs-map installeert. Uit: het spel maakt nooit contact met Modrinth.",
"horizon.extras.splitscreen.tooltip": "In een wereld kan een andere controller meedoen als nieuwe speler: druk op A wanneer dit wordt gevraagd, kies vervolgens een profiel en waar je wilt spelen.",
"horizon.splitscreen.setup.monitor_unknown": "een monitor",
@@ -467,8 +466,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Verborgen: elke Horizon-speler ziet je zonder je cosmetica, inclusief jij. Dezelfde schakelaar als de kledingkast van de draagraket.",
"horizon.extras.cosmetics_others": "Cosmetica van andere spelers",
"horizon.extras.cosmetics_others.tooltip": "Wat andere Horizon-spelers dragen: alles, alleen hun naamplaatjestijlen, of niets. Verborgen vraagt het spel nooit wat ze dragen.",
"horizon.extras.cosmetics_auras": "De aura's en sporen van anderen",
"horizon.extras.cosmetics_auras.tooltip": "Aura's en sporen die andere spelers dragen. Standaard uitgeschakeld.",
"horizon.extras.cosmetics_hide_fx": "Sporen en aura's van andere spelers verbergen",
"horizon.extras.cosmetics_hide_fx.tooltip": "Sporen en aura's van andere spelers blijven verborgen; hun andere cosmetica blijven zichtbaar. Standaard uit.",
"horizon.extras.cosmetics.shown": "Getoond",
"horizon.extras.cosmetics.hidden": "Verborgen",
"horizon.extras.cosmetics.name_tags": "Alleen naamplaatjes"
@@ -309,7 +309,6 @@
"horizon.osk.close": "Lukk",
"horizon.extras.chat_animation.tooltip": "En ny chat-linje glir inn fra venstre mens den toner inn, i stedet for å virke hel.",
"horizon.extras.chat_indicator.tooltip": "Den fargede linjen til venstre for en chattelinje.",
"horizon.extras.wheel_cycles_settings.tooltip": "Rullehjulet over en knapp i innstillingene endrer verdien, som i vanilje. Av, hjulet ruller bare listen.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs får en Legg til-knapp som søker i Modrinth og installerer en pakke i pakkemappen din. Av, spillet kontakter aldri Modrinth.",
"horizon.extras.splitscreen.tooltip": "I en verden kan en annen kontroller bli med som en ny spiller: trykk A når den spør, velg deretter en profil og hvor du vil spille.",
"horizon.splitscreen.setup.monitor": "Overvåk %s",
@@ -449,8 +448,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Skjult: hver Horizon-spiller ser deg uten kosmetikken din, inkludert du. Samme bryter som bærerakettens garderobe.",
"horizon.extras.cosmetics_others": "Andre spilleres kosmetikk",
"horizon.extras.cosmetics_others.tooltip": "Hva andre Horizon-spillere har på seg: alt, bare navnelappene deres, eller ingenting. Skjult spør spillet aldri hva de har på seg.",
"horizon.extras.cosmetics_auras": "Andres auraer og stier",
"horizon.extras.cosmetics_auras.tooltip": "Auraer og stier andre spillere har på seg. Av som standard.",
"horizon.extras.cosmetics_hide_fx": "Skjul andre spilleres spor og auraer",
"horizon.extras.cosmetics_hide_fx.tooltip": "Andre spilleres spor og auraer forblir skjult; deres andre kosmetikk vises fortsatt. Av som standard.",
"horizon.extras.cosmetics.shown": "Vist",
"horizon.extras.cosmetics.hidden": "Skjult",
"horizon.extras.cosmetics.name_tags": "Kun navnelapper",
@@ -314,7 +314,6 @@
"horizon.osk.close": "Barrar",
"horizon.extras.chat_animation.tooltip": "Una novèla linha de chat glissa de l'esquèrra del temps qu'esvaís, en luòc d'aparéisser entièra.",
"horizon.extras.chat_indicator.tooltip": "La barra colorada a esquèrra d'una linha de chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "La ròda de desfilament sus un boton dins los paramètres càmbia sa valor, coma dins la vanilha. Desactivat, la ròda desfila pas que la lista.",
"horizon.extras.pack_browser.tooltip": "Seleccionar los paquets de ressorsas obten un boton Apondre que cèrca Modrinth e installa un paquet dins vòstre dorsièr de paquets. Apagat, lo jòc contacta pas jamai Modrinth.",
"horizon.extras.splitscreen.tooltip": "Dins un mond, un autre contrarotlaire pòt rejónher coma jogaire novèl: quichatz sus A quand demanda, puèi causissètz un perfil e ont jogar.",
"horizon.splitscreen.setup.monitor_unknown": "un ecran",
@@ -465,8 +464,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Amagat: cada Horizon jogaire vos vei sens vòstres cosmetics, vos tanben. Lo meteis interruptor que l'armari del lançaire.",
"horizon.extras.cosmetics_others": "Los cosmetics dels autres jogaires",
"horizon.extras.cosmetics_others.tooltip": "Çò que pòrtan los autres jogaires Horizon : tot, sonque lors estils de etiqueta de nom, o res. Amagat, lo jòc demanda pas jamai çò que pòrtan.",
"horizon.extras.cosmetics_auras": "Las auras e las pistas dels autres",
"horizon.extras.cosmetics_auras.tooltip": "Auras e pistas que los autres jogaires pòrtan. Desactivat per defaut.",
"horizon.extras.cosmetics_hide_fx": "Amagar las traças e las auras dels autres jogaires",
"horizon.extras.cosmetics_hide_fx.tooltip": "Las traças e las auras dels autres jogaires demòran amagadas; lors autres cosmetics se vesan totjorn. Desactivat per defaut.",
"horizon.extras.cosmetics.shown": "Mostrat",
"horizon.extras.cosmetics.hidden": "Amagat",
"horizon.extras.cosmetics.name_tags": "D'etiquetas de nom solament"
@@ -334,7 +334,6 @@
"horizon.osk.close": "Fechar",
"horizon.extras.chat_animation.tooltip": "Uma nova linha de bate-papo aparece da esquerda enquanto desaparece, em vez de aparecer inteira.",
"horizon.extras.chat_indicator.tooltip": "A barra colorida à esquerda de uma linha de bate-papo.",
"horizon.extras.wheel_cycles_settings.tooltip": "A roda de rolagem sobre um botão nas configurações altera seu valor, como no vanilla. Desligado, a roda apenas rola a lista.",
"horizon.extras.pack_browser.tooltip": "Select Resource Packs obtém um botão Adicionar que pesquisa Modrinth e instala um pacote em sua pasta de pacotes. Desligado, o jogo nunca entra em contato com Modrinth.",
"horizon.extras.splitscreen.tooltip": "Num mundo, outro controlador pode entrar como novo jogador: pressione A quando solicitado e escolha um perfil e onde jogar.",
"horizon.splitscreen.setup.monitor": "Monitorar %s",
@@ -484,8 +483,8 @@
"horizon.extras.cosmetics_mine.tooltip": "Oculto: todo jogador do Horizon vê você sem seus cosméticos, inclusive você. A mesma opção do guarda-roupa do lançador.",
"horizon.extras.cosmetics_others": "Cosméticos de outros jogadores",
"horizon.extras.cosmetics_others.tooltip": "O que os outros jogadores do Horizon usam: tudo, apenas os estilos dos crachás ou nada. Escondido, o jogo nunca pergunta o que eles vestem.",
"horizon.extras.cosmetics_auras": "Auras e trilhas de outros",
"horizon.extras.cosmetics_auras.tooltip": "Auras e trilhas que outros jogadores usam. Desativado por padrão.",
"horizon.extras.cosmetics_hide_fx": "Ocultar rastros e auras de outros jogadores",
"horizon.extras.cosmetics_hide_fx.tooltip": "Os rastros e auras de outros jogadores ficam ocultos; os demais cosméticos deles continuam visíveis. Desativado por padrão.",
"horizon.extras.cosmetics.shown": "Mostrado",
"horizon.extras.cosmetics.hidden": "Oculto",
"horizon.extras.cosmetics.name_tags": "Somente tags de nome"
@@ -293,7 +293,6 @@
"horizon.osk.close": "Fechar",
"horizon.extras.chat_animation.tooltip": "Uma nova linha de chat desliza a partir da esquerda enquanto aparece gradualmente, em vez de surgir por inteiro.",
"horizon.extras.chat_indicator.tooltip": "A barra colorida à esquerda de uma linha de chat.",
"horizon.extras.wheel_cycles_settings.tooltip": "A roda do rato sobre um botão nas definições altera o seu valor, tal como no vanilla. Se desligado, a roda apenas percorre a lista.",
"horizon.extras.splitscreen.tooltip": "Num mundo, outro comando pode juntar-se como novo jogador:prime A quando for pedido, depois escolhe um perfil e onde jogar.",
"horizon.splitscreen.setup.monitor_unknown": "um monitor",
"horizon.splitscreen.setup.window": "Janela %s",
Loaded 100 of 565 files, more files were not shown because too many files have changed in this diff. Show more