Compare commits

..
Author SHA1 Message Date
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
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
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
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
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 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
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 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
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 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
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
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
Ludovic Marie 86a9d9e669 Merge pull request #10236 from horizon-rust/fix/launcher-screenshot-memory
🤖 Agent: merging. The screenshot tiles, viewer and background no longer go through GPUI's never-evicted img(path) cache. Two 32k×16k screenshots on Home went from an 8.7 GiB peak to 277 MiB on the desktop. One risk to watch: the 2 s grace lets the budget overflow while more than 256 MiB of tiles is on screen at once (about 280 tiles with Show all), which is bounded by the list itself.
2026-11-05 21:37:05 +01:00
Horizon agentandClaude Opus 5.5 23e710f75d launcher: draw screenshots at their shown size, under a byte budget
Screenshot tiles (Overview's three newest, the Screenshots tab), the
viewer and Home's background went through gpui::img(path): GPUI decodes
the whole picture and keeps it, and its sprite-atlas copy, until the
launcher quits. One 32768x16384 screenshot is 2 GiB decoded; with two
of them Home alone reached 8.7 GiB (24 GiB reported on Linux).

- pictures (lib): PNGs are read a row at a time and box-filtered down to
  the drawn size, never held whole; other formats decode whole only
  under 256 MiB. Lru keeps made pictures under a byte budget, sparing
  those drawn in the last 2 s.
- picture_cache: makes them off the UI thread (3 at once), 640 px for
  tiles, 3840 px for the viewer and background, 256 MiB budget; evicted
  images are dropped from the window's sprite atlas.
- glass_shot_blur's blurred copies go through it too, from a shrunk read
  instead of image::open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:35:43 +01: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
Ludovic Marie 3d33b0299d Merge pull request #10223 from horizon-rust/feat/shop-tab-button-label
🤖 Agent: the Wardrobe button opens the launcher's Shop tab now, so its label drops "on playhorizon.dev"; the key is renamed in place in all 86 tables (no stale key left), and the i18n test passes.
2026-11-05 21:20:36 +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 2d0c715866 Launcher Wardrobe: the Shop button reads "Get more cosmetics"
It opens the launcher's Shop tab now, not the website, so the old label
"Get more cosmetics on playhorizon.dev" was wrong. The key is renamed in all 86
tables, in place.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 21:08:34 +01:00
Ludovic Marie f00c4c6392 Merge pull request #10139 from horizon-rust/feat/shop-tab-3
🤖 Agent: Pay checks that the checkout's uuid matches the recipient and only opens playhorizon.dev/shop/pay with a validated txn_ id; no card entry in the launcher.
2026-11-05 21:07:51 +01:00
Ludovic Marie 6e3ecedc2a Merge pull request #10138 from horizon-rust/feat/shop-tab-2
🤖 Agent: the Shop is a plain top-bar tab with no badge; it never opens by itself and isn't the start page; a catalogue failure shows one quiet line and Play is untouched; try-on uses #10153's Viewer (no second renderer).
2026-11-05 21:05:28 +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
Ludovic Marie c58f498288 Merge pull request #10137 from horizon-rust/feat/shop-tab-1
🤖 Agent: shop price rules (sets partly owned, items inside a set in the basket, Supporter once) match the site's pricing.ts and are tested against hand-worked values; nothing player-visible yet. Requeued after merging main (#10153's lang tails).
2026-11-05 20:39:00 +01:00
Ludovic Marie 1fdf875085 Merge pull request #9918 from oxi-does-dev/fix/interact-hit-location
🤖 Agent: Requeued on the same head: the parity run died in 1 s on the runner's missing .horizon-prepare.lock (environment, no harness ran), so it's rerunning (42040505129). The interact packet now carries the ray's hit point from the mob's feet. Union on main dd675b574 green.
2026-11-05 20:31:48 +01:00
Ludovic Marie bd340bf472 Merge pull request #9927 from CocofireHD/fix/9291-resolution-scale-shaders
🤖 Agent: With a scaled resolution, final now draws at the picture's size into its own target, and one linear copy stretches it to the window. Same-size output and packs without a final keep their old paths, so only resolution-scaled pack users take the extra pass. Union on main dd675b574 green (fmt, clippy, 4426 client tests including the render crate).
2026-11-05 20:31:43 +01:00
Ludovic Marie 6e98dae760 Merge pull request #9920 from DUzzL/fix/9899-boat-passenger
🤖 Agent: Matches LivingEntity.calculateEntityAnimation: a passenger calls walkAnimation.stop(), which zeroes speed, old speed and position, so the seat moving with the boat no longer swings the legs. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
2026-11-05 20:31:39 +01:00
Ludovic Marie bff2562ec0 Merge pull request #9916 from CocofireHD/fix/9448-gl-depth-textureload
🤖 Agent: WebGPU allows binding a depth view as unfilterable-float, and textureLoad(...).r is the same texel, so Vulkan, DX12 and Metal read the same depth. The translation test now fails on any GL-run entry point that doesn't translate, except the one known glint case. Union on main dd675b574 green (fmt, clippy, 4426 client tests).
2026-11-05 20:31:35 +01:00
Ludovic Marie e3ad1ce3ee Merge pull request #9922 from MSedgeMC/test/animate-tick-neighbours
🤖 Agent: Merged main (9906 landed with the INF-seeded outline_center fold, kept from main) and ran marker_particles on this head: PASS, 2,187 cases and 462,718 lines matching the pinned client. The hive's loop now draws nextInt(1) again on the check that ends it, as BeehiveBlock.animateTick's for-condition does.
2026-11-05 20:27:42 +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
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
Ludovic Marie 5b4823c198 Merge pull request #10093 from horizon-rust/emotes/3-network
🤖 Agent: emotes, part 3 (#10069): others see your emote through the cosmetics service, now based on main. CI verify is green. Polling is bounded: at most once a second, only while Horizon players are in view, and only with Other players' cosmetics on Shown. Sending is signed and only while Show me as Horizon is on. Without horizon-cosmetics#14 deployed, the service answers 404 and the game backs off.
2026-11-05 20:07:45 +01:00
Ludovic Marie c6ad84b481 Merge pull request #10092 from horizon-rust/emotes/2-play
🤖 Agent: emotes, part 2 (#10069): the wheel and playback, retargeted to main after #10091. CI verify is green. Two clients played every emote in game; captures are in the PR. Visual only, nothing sent to game servers. #10093 follows.
2026-11-05 20:05:22 +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
Ludovic Marie 0508903777 Merge pull request #10091 from horizon-rust/emotes/1-format
🤖 Agent: emote format, the bottom of the emotes stack (#10069). pr_ready on the whole stack is green except the NixOS-only uname test. pose_body copies the target at full weight: 0.26–1.1 µs per emoting player and frame, measured. #10092 follows once this lands.
2026-11-05 20:01:16 +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 e7c5cb7331 Merge main (via feat/shop-tab-2) into feat/shop-tab-3
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:54:27 +01:00
Horizon agentandClaude Opus 5.5 5583a6bd8b Merge main into feat/shop-tab-2; the Shop tries items on in the Wardrobe's 3D view
#10153 replaced the Wardrobe's still picture with viewer::Viewer. The Shop now
uses its own Viewer (drag to turn, scroll to zoom, Reset view) instead of the
still picture and its Turn button, so there is one renderer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:54:15 +01:00
Horizon agentandClaude Opus 5.5 e15d46d371 Merge main into feat/shop-tab-1
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:52:52 +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
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
Ludovic Marie f2f95dad8a Merge pull request #10153 from horizon-rust/agent/wardrobe-3d
🤖 Agent: verified on the desktop at this tree: horizon-cosmetics fmt/clippy/tests (40) and the launcher's fmt/clippy/tests (695) green; pr_ready skipped because the desktop disk was under 45 GB. Point to watch: preview::render now takes a Camera (launcher is its only caller), and lib.rs will conflict by one line with the emote format PR #10091.
2026-11-05 19:38:53 +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
Ludovic Marie 56349b9713 Merge pull request #9926 from masa-masa-0314/feat/realms-settings-tab
🤖 Agent: Fixed to the 26.3 jar's DTOs: the region preference travels as its id (0/1/2) and the region as its nameId, so a Realm that has a preference parses again (it used to fail the whole RealmServer parse). preferredRegions reads regionDataList/regionName, and the configuration POST puts the slot under options with update_slot's RealmsSlotUpdateDto. New tests pin the POST body and the parsing; CI verify is green on this head.
2026-11-05 19:34:24 +01:00
Horizon agentandClaude Opus 5.5 4f0af1f5f2 Merge main (via feat/shop-tab-2) into feat/shop-tab-3
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:29:24 +01:00
Horizon agentandClaude Opus 5.5 75adf421e0 Merge main (via feat/shop-tab-1) into feat/shop-tab-2
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:29:22 +01:00
Horizon agentandClaude Opus 5.5 ec78951af1 Merge main into feat/shop-tab-1 (lang tables re-appended)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:29:20 +01:00
Horizon agentandClaude Opus 5.5 85056bcaaf Realms settings: rustfmt
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:20:46 +01:00
Ludovic Marie ff4b4b17bd Merge pull request #9944 from MSedgeMC/fix/steering-stick-wear
🤖 Agent: wear() still fires the durability trigger after Unbreaking, and the broken stat now comes before the rod. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
2026-11-05 19:09:06 +01:00
Ludovic Marie 120fe4d7e3 Merge pull request #9941 from MSedgeMC/fix/bundle-tooltip-display
🤖 Agent: The height probe and the draw share bundle_tooltip, so a hidden grid can't leave an empty box taller than its lines. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
2026-11-05 19:09:02 +01:00
Ludovic Marie bebaecd65a Merge pull request #9939 from degepie/fix/name-tag-interact-trigger
🤖 Agent: The tag is copied before name_mob uses it up, like handleInteract's itemStackBefore. Follow-up gap, not this PR: vanilla also fires the trigger with an empty stack on non-item successes. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
2026-11-05 19:08:58 +01:00
Ludovic Marie 674de01447 Merge pull request #9938 from degepie/fix/drink-consume-trigger
🤖 Agent: Same two calls finish_consuming makes, before the stack shrinks, as Consumable.onConsume orders them. drink_ominous_bottle awarded neither, so nothing counts twice. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
2026-11-05 19:08:52 +01:00
Ludovic Marie d1dc1cb937 Merge pull request #9937 from degepie/fix/structure-tag-predicates
🤖 Agent: The pack-less structure tags are a OnceLock keyed on the first resource root, as StructureSpawns::shared is. location_structure_tags_hold_their_structures passes with prepared resources. The parity failure at ~17:00 was the playhorizon runner's dead .horizon-prepare.lock link, not this change: rerun on this head.
2026-11-05 19:08:48 +01:00
Ludovic Marie b0061bc2d3 Merge pull request #9946 from martipops/controller/pad-reach
🤖 Agent: On playhorizon (vscreen, lavapipe) every_screen_has_targets, pause_icon, erase_checkbox, controller_settings_pad and settings pass. The empty-label target can add a second target on a rect that already called target_as: drawn_targets dedups only against labelled widgets, not between targets. Nothing on main seems to do that today. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
2026-11-05 19:08:44 +01:00
Horizon agentandClaude Opus 5.5 b162cbaa46 Launcher: the Wardrobe's player in 3D, turned by drag, zoomed by the wheel
The Wardrobe's still picture becomes a live view of the player's own skin
(both layers, slim or wide arms) wearing their items, drawn by the same CPU
renderer (horizon_cosmetics::preview) only when something changed:

- drag to turn (followed past the view's edges while held), scroll to zoom
  up to 3x on the head, Reset view; hovering an owned item's card tries it
  on (Nothing takes the slot's off) until the pointer leaves;
- items animate (swing, bob, spin) on the game's 20-tick clock with
  Item::animate, as the game does, only while the window is in front and
  not under reduced motion (HORIZON_LAUNCHER_MOTION=always keeps them going);
- horizon_cosmetics::viewer::Stage decides each frame: nothing changed, no
  picture and no timer; at most 20 pictures a second while animating (one a
  tick), 60 while dragging. Pictures are drawn at the view's size in points
  (240x330, about 2 ms) and baked up to device pixels;
- preview::Camera holds the turn, tilt and zoom (with headroom above the
  head for tall hats and back items), Draw::item_posed keeps items on posed
  bones, and viewer::BodyMotion is the hook an emote plays through
  (Viewer::play), for the emote format in flight (#10091).

The view is a GPUI view of its own, so a new picture rebuilds it alone;
the pointer is handled by the page's element around it, since GPUI Fast
hit-tests the page's own elements. "Turn" gives way to "Reset view" and a
hint, in every language.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:08:41 +01:00
Ludovic Marie 0ad6666970 Merge pull request #9942 from TheMrZZ/fix/enderman-neighbour-placement
🤖 Agent: Deferring the attempt to the event phase lets update_from_neighbour_shapes see the real level and ticks, as EndermanLeaveBlockGoal.tick does. leave_block_goal_uses_real_neighbor_shapes_and_keeps_air_placement passes with prepared resources. Still missing: canSurvive and the BLOCK_PLACE game event. Union run on playhorizon (main a1e0c0db0 + this gate's 11 PRs): fmt, clippy, all touched crates' tests pass.
2026-11-05 19:08:39 +01:00
Horizon agent 1011abe707 Merge branch 'emotes/2-play' into emotes/3-network 2026-11-05 19:04:32 +01:00
Horizon agentandClaude Opus 5.5 6f88fac9a9 Emotes: the wheel's key release and click in emote_wheel.rs (keeps game.rs's release block whole)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 19:04:32 +01:00
Ludovic Marie baa9153c82 Merge pull request #9943 from CocofireHD/fix/9439-tank-refuse-fluid
🤖 Agent: native_fluid_at now allocates the block id String on every call, drains and tank queries included. That's fine for mod helpers, but worth an &'static str if a tick path ever calls it. The fragment stamps (tests.test_stamp_native_hooks passed in the union run). I ran the native_fluids tests on this head on playhorizon, 12 passed, the new dispatch test included.
2026-11-05 19:03:54 +01: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
Ludovic Marie 936594e71b Merge pull request #10078 from horizon-rust/pvp/hud-layout
🤖 Agent: merging. The layout lines only add keys to horizon-extras.txt; files without them load as before (every widget at its default place and style). Verify is green; stacked on #10077.
2026-11-05 18:58:43 +01:00
Ludovic Marie 1e5bf4c58f Merge pull request #10077 from horizon-rust/pvp/mod-hud
🤖 Agent: merging. Every widget reads local state only (no packets, no server queries); with every widget off, draw_pvp_hud returns after Hud::any(), so a player who never turns one on pays nothing. Verify is green; pr_ready --quick on playhorizon fails only the render crash:: tests that SIGSEGV under llvmpipe there, and crash.rs is untouched.
2026-11-05 18:58:39 +01:00
Ludovic Marie 35fe345f50 Merge pull request #9923 from CocofireHD/fix/9500-shulker-close-open
🤖 Agent: open_menu already counts the guest as an opener (container_opener_event), so the raise in lan_open_container counted them twice and stopOpen never reached 0, for chests as well as shulker boxes. The author's lan_e2e shulker_menu check pins the lid open, then shut, on both host and guest. Union on main dd675b574 green.
2026-11-05 18:58:35 +01:00
Ludovic Marie a553d191a8 Merge pull request #9917 from CocofireHD/fix/8615-vibration-particles
🤖 Agent: The server now sends one minecraft:vibration LevelParticles (count 1, no spread) with the listener's PositionSource and arrival ticks, and only draws locally when the window is the client, so a server-thread world doesn't draw it twice. Tested at a729b3cfe in a union on main dd675b574 (fmt, clippy, 4426 client tests green); this head only adds the author's main merge (test helper conflict) on top.
2026-11-05 18:58:26 +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
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
CocofireHDandopencode 1c712e434a Mod fluid tanks: prove a hook refusal through the dispatch
A focused test drives the public block_entity_accepts_fluid dispatch, as the game does: a water tank that refuses a mod's fluid leaves both insert passes empty and offers native_move_fluids no room, while water and a tank with no hook still fill.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 18:34:29 +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 ce9d8ae6cf Emotes: the measured cost per frame in docs/cosmetics.md
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:23:53 +01:00
Horizon agentandClaude Opus 5.5 4a59bae751 Emotes: the wheel reads the cursor as it is (clippy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:23:53 +01:00
Horizon agentandClaude Opus 5.5 95244a792a Emotes: others see yours (the cosmetics service), the wheel lists the account's
- Mine: while an emote plays, the game tells the cosmetics service, signed in as for "Show me
  as Horizon" and only while that is on: `POST /v1/emote`, with a session kept an hour
  (`account::Kept`). A loop is said again every 30 s.
- Theirs: `horizon_cosmetics::Feed`, a thread, asks `POST /v1/emotes` which Horizon players in
  view play one, at most once a second and only while some are in view, then fetches the item
  files once. On failure it waits 15 s, doubling up to 10 min. Their models play the emote from
  where it is; a player who moves ends theirs at once.
- Follows "Other players' cosmetics": with anything but Shown, nothing is asked and nothing is
  drawn.
- The wheel: the account's emotes (`GET /v1/me`'s `emotes`), read when it opens, at most every
  10 minutes.

Nothing reaches game servers; vanilla clients see nothing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:23:53 +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 ff8997168c Emotes: horizon-ctl emote in the generated command reference
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:08:59 +01:00
Horizon agentandClaude Opus 5.5 2fed27fce9 Emotes: play one from the emote wheel, on your model and in the camera
- The wheel: Emote Wheel (`key.emote`, B by default, unbound on a controller; rebindable in
  Controls) shows up to 8 emotes around the middle of the screen. To play one: hold the key and
  release on an emote, click one, or press its number.
- The emote cancels (and fades out) on movement, an attack, mining, using an item, being hurt,
  riding, sleeping, gliding or sneaking. Only a standing player starts one.
- From first person, the camera turns to the front view and eases out from close for the emote,
  then comes back.
- Bones: `anim::setup` lays the emote over vanilla's animation, keyed bones only.
- Props: drawn with the cosmetics, growing in and out with the blend.
- `horizon-ctl emote [ID|stop]`.
- HORIZON_COSMETICS_LOCAL: the wheel offers the folder's emotes, and mannequins play its first
  one (the art and the bench).

Visual only: nothing reaches the game server.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:08:59 +01:00
Horizon agentandClaude Opus 5.5 44c14af45a Emotes: fully in, the bones take the target as it is (no quaternion round trip)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:08:58 +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
Horizon agentandClaude Opus 5.5 071feb3308 Cosmetics: the emote format (keyframed bones, root, props)
An item of the new `emote` slot carries an `emote` block: per-part tracks of keys (rotation,
offset, a prop's scale) eased smooth (Catmull-Rom), linear, in_out or step, one-shot or looping,
faded in and out over `blend` ticks. `Item::parse` checks its limits (200 ticks, 16 tracks,
32 keys a track and 192 in all, 360° turns and 8 px moves, the root 720° and 16 px, props'
reach through every key). `Emote::pose_body` poses the six bones (keys replace a bone's
rotation, keyed bones only), `pose_props` the props, and `Playing` gives an emote's time and
weight, its cancel included. Visual only, nothing random.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:04:09 +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 1fcc43f60f Realms settings: the 26.3 JSON shapes for the region preference and the configuration
RegionSelectionPreference travels as its id (0, 1, 2) and the region as its nameId, so a
Realm with a preference no longer fails to parse as a String. PreferredRegionsDto reads
regionDataList / regionName / serviceQuality, RealmsConfigurationDto posts the slot under
options with update_slot's RealmsSlotUpdateDto (worldTemplateId, worldTemplateImage), a Realm
without a preference is automatic by owner (RegionSelectionPreferenceDto.DEFAULT), and the
labels use the realms.configuration.region* keys vanilla's settings tab shows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:58:19 +01: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 ad73e4a692 Launcher Shop: a basket, gifts, the Supporter badge, and Pay in the browser
Add to basket on the chosen card, the basket's lines and total with the
service's rules (owned items and items inside a set in the basket cost
nothing), a gift to a friend looked up by name, the Supporter badge's
amount within the catalogue's range, and "Ask whoever pays before you
buy. Refunds within 14 days for any reason." by Pay, which starts
Paddle's checkout on the service and opens playhorizon.dev/shop/pay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:52:57 +01:00
Horizon agentandClaude Opus 5.5 fac063268c Launcher: a plain Shop tab beside the Wardrobe
The catalogue's items, sets, collections and the Supporter badge as cards
with their prices, the chosen card tried on the account's own skin, and
the founder set's claim (one click where Paddle doesn't sell, else
Paddle's free checkout in the browser). No badge on the tab, it never
opens by itself, and a catalogue that doesn't load says so in one line.
The Wardrobe's one shop button now opens this tab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:51:19 +01: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
Horizon agent 621d47ab6d Merge commit 'dd675b574115536446647051e29dac76f1055bc5' into cg51/pr-9922
# Conflicts:
#	crates/horizon-client/examples/probe/marker_particles_probe.rs
#	crates/horizon-client/src/animate_tick.rs
#	tools/test_marker_particles_parity.py
2026-11-05 17:50:19 +01:00
CocofireHD 49e8906c56 Merge remote-tracking branch 'origin/main' into fix/8615-vibration-particles 2026-11-05 17:47:05 +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 07c08509df Launcher Shop: prices, sets and the Supporter badge from the catalogue; the web checkout
The catalogue's shop fields (currencies, prices, sets, collections, the
Supporter badge), the player lookup and the web checkout in the wardrobe
client, and the basket's price rules in shop.rs, as the website applies
them. Nothing shows them yet: the Shop tab comes next.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:40:05 +01: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
CocofireHDandopencode 40eeef6f80 lan_e2e: state the shulker lid check in the harness docs
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 17:26:35 +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
CocofireHD a729b3cfe6 Merge remote-tracking branch 'origin/main' into fix/8615-vibration-particles 2026-11-05 17:18:40 +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
Horizon agentandClaude Opus 5.5 7b9daf7bbf PvP & HUD: document the widgets' style and layout
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:22 +01:00
Horizon agentandClaude Opus 5.5 afc6c3c43a PvP & HUD: free widget places, sizes, text colour, opacity and shadow
Each HUD widget can leave its corner for a place kept as fractions of the
screen's free room (so it stays on screen at any size) and a size from 50
to 200%, saved as hudPlace lines in horizon-extras.txt only once moved.
The page gains the boxes' opacity, the text colour and the text shadow.
Snapping (screen edges, middle, other boxes) is here for the editor.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:22 +01:00
Horizon agentandClaude Opus 5.5 69874aef48 PvP & HUD: no widget on, no HUD work; document the new widgets
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:17 +01:00
Horizon agentandClaude Opus 5.5 9f46032653 PvP & HUD: FPS, CPS, keystrokes, clock, reach and speed widgets
Six more widgets on the PvP & HUD page, off by default, each from the
client's own state: the frame counter, presses of the last second, the
movement and mouse keys while held, the local time, the distance of the
last hit the crosshair's ray measured, and horizontal speed sampled from
the player's position. Each caches its lines and rebuilds them only when
the shown value changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 17:10:17 +01:00
CocofireHDandopencode 463c970b13 Merge origin/main into fix/8615-vibration-particles
Resolved the one conflict, in game/lan_particles.rs: test_hub keeps the
shared helper the vibration test needs, and its guests now carry main's
Joined.expected field and bind through NetConfig::loopback. The
sculk_vibration LAN check survives main's rewrite of tools/lan_e2e.py.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 17:08:57 +01:00
CocofireHDandopencode 58263dc4a0 lan_e2e: capture the shulker lid open and closed
The shulker_menu check now keeps host and guest screenshots of the lid before and after the guest closes the menu, so a stuck lid is visible as well as asserted.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 17:03:25 +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
masa-masa-0314andZCode df263fef74 Realms: the settings tab and the region preference screen
The configuration screen's Settings tab (RealmsSettingsTab): the world's name and
description fields (the click moves the caret between them), the region preference line
and its Region Preference button, and the open/close toggle moved here from the footer
(vanilla keeps it on this tab). Leaving the tab — switching tabs or Back/Escape — saves
the changed name, description and preference through RealmsClient.updateConfiguration
(POST worlds/{id}/configuration with RealmsConfigurationDto's JSON: the active slot, its
settings, the preference and the description).

RealmsPreferredRegionSelectionScreen: the automatic choices and every manual region, a
click selects, Done writes the choice back to the tab and Cancel drops it.

RealmsClient gains getPreferredRegionSelections (GET regions/preferredRegions) and
updateConfiguration; RealmServer reads regionSelectionPreference; RegionSelectionPreferenceDto,
RegionDataDto and RealmSetting serialization join the DTOs.

The tier's render/simulation distance options and the region quality icons are the next
PRs (the icons need sprites the GUI atlas does not carry yet); the region rows say their
name for now.

Co-Authored-By: ZCode <[email protected]>
2026-11-05 23:24:38 +08: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
CocofireHDandopencode f8d0840cd7 lan_e2e: prove a sculk sensor's vibration reaches the host and a guest
VibrationSystem.Ticker.trySelectAndScheduleVibration sends one vibration particle to every player within 32 blocks; the check asserts both the host's window and a LAN guest make it (#8615).

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 15:30:45 +01:00
MSedgeMC e51fd1dff4 port_map: ItemStack.hurtAndConvertOnBreak is the steering stick's rod
The row was `missing`; the conversion was in `use_steering_stick` but
built a fresh plain rod and skipped `hurtAndBreak`.
2026-11-05 14:24:14 +00:00
MSedgeMC c135993a63 test: a steering stick that breaks leaves the rod the stick was
`transmuteCopyIgnoreEmpty`: at 18 + 7 of 25 the carrot on a stick becomes
a fishing rod that kept its custom name and starts whole, where the old
path wrote a fresh plain rod. Wear that Unbreaking took away (0) breaks
nothing, and a stack with no durability never breaks.
2026-11-05 14:24:12 +00:00
MSedgeMC c942e133da fix: a steering stick's wear goes through the item's durability rules
`FoodOnAStickItem.use` wears the stick with
`ItemStack.hurtAndConvertOnBreak`, which runs `hurtAndBreak` - and
`hurtAndBreak` asks `processDurabilityChange` first, so Unbreaking has its
say before any damage lands.

`use_steering_stick` added the whole 7 (carrot on a stick) or 1 (warped
fungus on a stick) straight onto `stack.damage`, so an Unbreaking stick
lasted exactly as long as a plain one, and it never reached the mods'
item-damaged hook.
2026-11-05 14:23:55 +00:00
CocofireHDandopencode 8df11ffe4c Mod fluid tanks: let a tank refuse a fluid
A mod tank can now refuse a fluid through native_block_entity_accepts_fluid (NeoForge's IFluidHandler.isFluidValid). The server asks it before filling a tank in both insert passes and before native_move_fluids counts a tank's room; every tank accepts every fluid unless a mod cancels. A water tank can take no create:chocolate, and a removed mod leaves no fluid id it cannot use behind.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 15:21:31 +01:00
MSedgeMC 3ef40bc92f port_map: BundleItem.getTooltipImage names the tooltip display rule
The row pointed at the geometry and carried the missing rule as a gap;
the grid now goes through `item_tooltip::bundle_tooltip`, which asks
`TooltipDisplay::shows("minecraft:bundle_contents")` before the layout.
2026-11-05 14:12:07 +00:00
MSedgeMC 875b657878 test: a bundle grid hidden by the tooltip display
`minecraft:bundle_contents` in `hidden_components`, and `hide_tooltip`,
both take the grid away; a display that hides `minecraft:lore` leaves it.
2026-11-05 14:11:58 +00:00
MSedgeMC 2ec1ce0480 fix: a bundle's contents grid follows the tooltip display
`BundleItem.getTooltipImage` returns nothing when the stack's
`minecraft:tooltip_display` hides `minecraft:bundle_contents`, so a
bundle given with `hide_tooltip` or with that component hidden drew no
grid. Horizon drew the grid for every bundle, and the tooltip box grew to
hold it.

`item_tooltip::bundle_tooltip` asks the display first and then defers to
`bundle::tooltip_layout`; the tooltip draw uses it for the height probe
and for the draw alike.
2026-11-05 14:11:58 +00:00
CocofireHDandopencode 4993bc83ac Shaders: upscale the scaled pack target to the full surface
A Resolution Scale leaves the pack's picture smaller than the window. Its final pass drew at the window's size, so a shader using gl_FragCoord as texel coordinates (Photon's final) only filled a quarter of the screen. Draw final at the picture's size into an offscreen texture and copy it once to the window with a screen-row fullscreen pass; the no-pack path and same-size output are unchanged.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 15:10:10 +01:00
Florian ERNSTandCodex eb0193cb34 Normalize Enderman carried states before placement
Run neighbor-shape updates on the authoritative level before the existing eligibility checks, and clear the verified carrier after the handled placement.

Co-Authored-By: Codex <[email protected]>
2026-11-05 15:06:31 +01:00
CocofireHDandopencode 2816f17962 Shulker boxes: close the block entity when its menu is removed
A guest's use raised its opener count twice: open_menu counted the block entity's startOpen, then lan_open_container counted it again once the menu was sent. Closing lowered it once, to one, so stopOpen never ran and the lid stayed open with its raised collision (#9500).

The guest open path now relies on the count open_menu already made; the harness keeps the lid open and closed checks for both host and guest.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 15:01:27 +01:00
MSedgeMC afad8241b6 port_map: the two BeehiveBlock tick methods
animateTick and trySpawnDripParticles are block_animate_tick and beehive_drip;
the animateTick row records the attempt bound the loop re-reads.
2026-11-05 14:00:10 +00:00
MSedgeMC 677e00c72f test: a floor, a fluid and a whole sample box for the animateTick zoo
The tick cases' stand-in world stopped at y 32..96 and x/z -32..31, one block
short of every sample the loop takes and of the neighbours those samples read,
so a block at the edge compared air against the oracle's unending world: that
is where the beehive, the redstone ore and the pointed dripstone differences
came from. Fill the whole box plus a block either way, let a case name the
floor below y = 64, hand the loop the state's own fluid (a waterlogged block
has a water source, which it ticks and drips) and bind the client jar's block
tags in the oracle the way a datapack load does, which a bare bootstrap leaves
empty. Then add the families that read a neighbour, a shape or their own fluid:
dried ghast over soul sand, soul soil, stone and air; hives and a nest embedded
and hanging; a lit redstone ore and a deepslate one embedded and over air; and
stalactite tips over stone and water. 2,097 cases and 456,747 output lines
become 2,187 and 462,718.
2026-11-05 14:00:10 +00:00
MSedgeMC 9d11ae8abb fix: a hive asks the level random for its attempt bound until the loop ends
BeehiveBlock.animateTick is a `for (i = 0; i < random.nextInt(1) + 1; i++)`
loop, so the bound is drawn again on the check that ends it: a hive always
spends two nextInt(1) draws around its single trySpawnDripParticles attempt.
Horizon hoisted the bound out of the loop and spent one, which left the level
random a draw ahead of vanilla from the first hive the ambient loop touched:
every particle after it moved, and the honey drip of a hive hanging over air
came out at the wrong column.
2026-11-05 14:00:10 +00:00
MSedgeMC 25cb339d57 port_map: WitherRoseBlock.animateTick is block_animate_tick
The row records where the smell test sits now: three smoke puffs about the
centre of the pick shape, moved by the flower's per-column model offset.
2026-11-05 14:00:10 +00:00
MSedgeMC 3555aa81d3 test: a wither rose world in the marker-particles parity suite
The corpus had no wither rose case at all. Add a world that is all wither rose,
run the whole ClientLevel.animateTick loop over it and compare every smoke puff
by exact position bits in call order: twenty-seven cases, three seeds and
twenty-four at random ones. The probe now supplies the flower's per-column model
offset the way the client's mesh tables do (a case without it would compare a
shape the game never shows). 2,070 cases and 402,440 output lines become 2,097
and 456,747.
2026-11-05 14:00:10 +00:00
MSedgeMC af128082f4 fix: centre the wither rose's smoke on its pick shape
WitherRoseBlock.animateTick reads its centre off getShape(state, level, pos,
CollisionContext.empty()), a flower's own box whose bounds centre is 0.5/0.5
before the block's per-column model offset. AnimateContext::shape_y answers with
the collision shape, which is empty for every flower, so the centre came out as
0 and the whole smoke column sat half a block towards the block's north-west
corner. Take the centre from the pick shape's boxes (BlockFacts::outline) and
keep the model offset on top of it.
2026-11-05 14:00:10 +00:00
DUzzL 61a74fdd0a Stop the walk animation of a player riding a boat 2026-11-05 14:48:21 +01:00
martipopsandClaude Opus 5.5 e58ee9c714 Controllers: icon buttons and two checkboxes take the focus, and a screen with no targets fails a test
An icon-only button recorded nothing, so the pad could not reach the pause
menu's Report Bugs, Feedback and Player Reporting icons, or Realms' icons. An
active button with an empty label is now a focus target. So are the
Multiplayer warning's "Do not show again" box and Optimize World's Erase
Cached Data box, which A now ticks.

tools/pad_menu_e2e.py gains every_screen_has_targets, which walks the title's
submenus and every options page and fails on a focus screen with no targets.
Replay Studio is allowlisted because it registers nothing yet. New pause_icon
and erase_checkbox cases press the new targets. The pause menu now has
unlabelled widgets, so splitscreen_e2e's setup scenario reads labels with get.
controller_settings_pad's check for a Navigation page now matches exactly: it
tripped on the General page's Navigation Repeat rows.

docs/controllers.md no longer says every rectangle registers itself or that
both modes reach every screen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 08:46:36 -05:00
CocofireHDandopencode 6492b16a7b Send minecraft:vibration particles from the server to nearby clients
Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 14:46:21 +01:00
CocofireHDandopencode 2cb008e118 Shaders: read depth as an unfilterable float so GLSL translates
The world's depth was bound as a depth texture and read with textureLoad, which naga's GLSL backend cannot emit, so TAA, the depth blit and the shader depth test failed to build on OpenGL. Bind those views as unfilterable float textures instead: wgpu accepts a depth view there, and textureLoad returns the same depth texel on every backend (Vulkan, DX12, Metal, GL).

The shader translation test now fails on any shader the GL backend runs that cannot translate, and pins fs_taa and fs_blit; the HUD's glint, which fails for an unrelated two-samplers-per-image naga limit, is the one allowed exception.

Co-Authored-By: opencode (DeepSeek V4.1 Flash) <[email protected]>
2026-11-05 14:45:43 +01:00
oxi-does-dev 0c897e3e33 Interact: send the point the ray hit, as vanilla does
MultiPlayerGameMode.interact sends EntityHitResult.getLocation() minus
entity.position() in every ServerboundInteractPacket. Horizon sent a
fixed [0.0, 1.0, 0.0] for mobs, a point in their middle: armour stands,
saddles, leashes and anything else that reads the click carried
interactAt to the wrong place, unlike vanilla, and a server's reach or
ghost-hand checks saw clicks that vanilla never makes.

mob_hit and the spear's attack_range pick now return the point along
the view ray where the mob's box was met, measured from its feet, and
every caller destructures the third element. A regression test checks
the packet's point against the ray.
2026-11-05 16:40:43 +03:00
degepieandClaude Opus 5.5 770e4461f8 Mobs: naming one or dyeing a sheep fires player_interacted_with_entity
ServerGamePacketListenerImpl.handleInteract fires the trigger with the
held stack as it was before the interaction whenever an item interaction
succeeds. NameTagItem.interactLivingEntity and DyeItem.interactLivingEntity
both succeed, but interact_mob returned after name_mob and after the sheep
dye before reaching the shared trigger call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-11-05 18:40:41 +05:00
degepieandClaude Opus 5.5 a85fb7c819 Advancement predicates read structure tags
A location predicate whose `structures` named a tag
(`#minecraft:shipwreck`, or a data pack's own `#pack:village`) never
matched: the predicates' tag lookup had no `worldgen/structure` case and
the world's pack tags didn't hold that registry. The world's tables now
load the structure tags with the packs' over the game's own, like the
entity type, biome and damage type tags, and a world without packs
reads the game's own structure tags (loaded once).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-11-05 18:26:17 +05:00
degepieandClaude Opus 5.5 ab4f48abfe Potions: drinking one awards the used statistic and fires consume_item
Consumable.onConsume awards the item's used statistic and fires the
consume_item trigger for a server player before the consumable's listeners
run and the stack shrinks. A potion and an ominous bottle finish through
finish_drinking, which did neither; food, honey bottles and milk go
through finish_consuming, which already does both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UE3PMKbnTcxfBSeT5uQWCK
2026-11-05 18:26:14 +05: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
488 changed files with 31631 additions and 2571 deletions

No files matched your search

+1
View File
@@ -35,6 +35,7 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
- **No hardcoded user-facing strings.** Keep UI translation-ready from the start. In the launcher (`launcher/`), wrap visible text with `tr("...")` or `trf("... {arg} ...", &[("arg", &val)])` from `crate::i18n` instead of raw string literals or `format!`. In the game, use translation keys (`Text::tr`, `lang::get`), never hardcoded English strings.
- **Claim before you start**: comment `/claim` on the issue (no issue yet: open one, then claim it). An assigned
issue is taken: pick another. `/unclaim` when you stop; a claim with no open PR linking it is released after 5 days.
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
Generated
+42
View File
@@ -329,6 +329,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"
@@ -1364,6 +1370,7 @@ dependencies = [
"serde_json",
"sha1",
"sha2",
"toml",
"unicode-bidi",
"ureq",
"wgpu",
@@ -1413,6 +1420,7 @@ dependencies = [
"horizon-client-render",
"horizon-command",
"horizon-core",
"horizon-cosmetics",
"horizon-dualsense",
"horizon-friends",
"horizon-hdr",
@@ -3714,6 +3722,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"
@@ -4167,6 +4186,7 @@ dependencies = [
"once_cell",
"rustls",
"rustls-pki-types",
"socks",
"url",
"webpki-roots 0.26.11",
]
@@ -4629,6 +4649,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"
@@ -4638,6 +4674,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"
+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 },
]
+143
View File
@@ -0,0 +1,143 @@
//! 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, 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);
Emote, "key.emote", Some(KeyCode::KeyB);
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;
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, KeyBinds, 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[..]);
for action in &names[mappings.len()..] {
assert!(action.starts_with("pad."), "{action}");
}
}
#[test]
fn an_actions_key_is_its_mappings_default_key() {
for (name, _, category) in 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 -33
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,15 +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
@@ -328,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),
}
@@ -340,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)
@@ -359,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 {
@@ -402,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,
}
}
@@ -591,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 {
@@ -621,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> {
@@ -697,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.
@@ -745,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;
@@ -1460,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!(
@@ -1579,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"]);
@@ -1597,6 +1752,36 @@ mod tests {
}
}
#[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();
@@ -1618,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());
}
@@ -1676,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]);
@@ -2206,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();
@@ -23,6 +23,8 @@ 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"),
(
@@ -440,6 +442,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()
@@ -503,6 +517,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;
+2
View File
@@ -18,6 +18,8 @@ horizon-client-core = { path = "../horizon-client-core" }
horizon-client-input = { path = "../horizon-client-input" }
horizon-client-render = { path = "../horizon-client-render" }
horizon-command = { path = "../horizon-command" }
# Emotes pose the player model (`Mob::emote`, `mobs::anim::setup`).
horizon-cosmetics = { path = "../horizon-cosmetics" }
horizon-items = { path = "../horizon-items" }
horizon-core = { path = "../horizon-core" }
# `horizon-ctl mcp`: the Model Context Protocol, shared with the launcher and the server.
+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,
+40 -22
View File
@@ -164,10 +164,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.
@@ -230,12 +230,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,
@@ -334,7 +334,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 +353,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 +367,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 +421,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),
@@ -452,6 +456,7 @@ impl Extras {
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 +603,7 @@ pub enum Item {
/// Shown only while cosmetics are enabled (`horizon_cosmetics::enabled`).
CosmeticsMine,
CosmeticsOthers,
CosmeticsAuras,
CosmeticsHideFx,
CosmeticsSolo,
HorizonMarks,
HorizonMe,
@@ -638,7 +643,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 +677,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 +705,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 +752,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 +771,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 +839,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 +879,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 +909,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 +974,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 +986,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 +1004,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 +1063,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 +1081,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 +1125,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(),
+33 -4
View File
@@ -12227,7 +12227,8 @@ mod tests {
}
}
/// Every WGSL shader file translates to HLSL as wgpu's DirectX 12 backend
/// writes it for FXC (shader model 5.1), entry point by entry point.
/// writes it for FXC (shader model 5.1), entry point by entry point, and
/// every shader the OpenGL backend runs also translates to GLSL.
#[test]
fn shaders_translate_to_hlsl_for_directx_12_and_glsl_for_opengl() {
fn wgsl_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
@@ -12277,6 +12278,14 @@ mod tests {
}
}
let mut glsl_checked = 0;
// Shaders the OpenGL backend runs must translate: skipping the ones
// that fail hides exactly the bug where a shader GL needs cannot
// compile (#9448).
let mut glsl_failed = Vec::new();
let mut translated = std::collections::HashSet::new();
// naga's GLSL writer gives an image one sampler, and the HUD's glint
// samples one image through two; one failed entry does not hide the rest.
let known_unsupported = [("hud.wgsl", "fs_cit_glint")];
for (path, text) in sources {
// entity.wgsl comes after the overlay's constants (entity_shader).
let module = naga::front::wgsl::parse_str(&text)
@@ -12377,11 +12386,18 @@ mod tests {
Default::default(),
)
.and_then(|mut w| w.write());
// Some shaders use features GLSL cannot express (depth textureLoad): GL does
// not run them. The ones it does run must translate, checked below.
if glsl_result.is_err() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if let Err(e) = glsl_result {
if !known_unsupported.contains(&(name, entry.name.as_str())) {
glsl_failed.push(format!(
"{} {} (GLSL {version}): {e}",
path.display(),
entry.name
));
}
continue;
}
translated.insert((name.to_owned(), entry.name.clone()));
glsl_checked += 1;
assert!(
!glsl.contains("ARB_arrays_of_arrays"),
@@ -12392,6 +12408,19 @@ mod tests {
}
}
}
assert!(
glsl_failed.is_empty(),
"shaders the OpenGL backend runs must translate:\n{}",
glsl_failed.join("\n")
);
// These read the world's depth through `textureLoad`; before #9448 their
// depth binding could not translate, so TAA and the blit never came up.
for (file, entry) in [("world_post.wgsl", "fs_taa"), ("pack_post.wgsl", "fs_blit")] {
assert!(
translated.contains(&(file.to_owned(), entry.to_owned())),
"{file} {entry} must translate to GLSL"
);
}
assert!(glsl_checked >= 10, "{glsl_checked} GLSL translations");
}
#[test]
+1 -1
View File
@@ -287,7 +287,7 @@ impl Cull {
0,
fragment,
wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
@@ -114,8 +114,10 @@ fn see(t: u32) {
}
}
// Phase 1's depth, where the test runs in the shader (gfx/cull.rs).
@group(1) @binding(0) var depth: texture_depth_2d;
// Phase 1's depth, where the test runs in the shader (gfx/cull.rs). A depth
// view bound as an unfilterable float: naga's GLSL backend cannot `textureLoad`
// a depth texture, and the texel is the same value.
@group(1) @binding(0) var depth: texture_2d<f32>;
// The world pass's reversed depth test, greater or equal passing, made here:
// where it cannot be forced before a shader that writes storage, a fragment
@@ -123,7 +125,7 @@ fn see(t: u32) {
// runs only for the fragments that pass it.
@fragment
fn fs_box(in: Box) {
if in.position.z >= textureLoad(depth, vec2<u32>(in.position.xy), 0) {
if in.position.z >= textureLoad(depth, vec2<u32>(in.position.xy), 0).r {
see(in.test);
}
}
@@ -52,7 +52,9 @@ struct Params {
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var linear_clamp: sampler;
@group(0) @binding(2) var input_depth: texture_depth_2d;
// A depth view bound as unfilterable float: naga's GLSL backend cannot
// `textureLoad` a depth texture, and the texel is the same value.
@group(0) @binding(2) var input_depth: texture_2d<f32>;
@group(0) @binding(3) var current_color: texture_2d<f32>;
@group(0) @binding(4) var previous_color: texture_2d<f32>;
@group(0) @binding(5) var dilated_depth_out: texture_storage_2d<r32float, write>;
@@ -333,7 +335,7 @@ fn load_input_depth(p: vec2<i32>) -> f32 {
if !is_on_screen(p, params.render_size) {
return 0.0;
}
return textureLoad(input_depth, p, 0);
return textureLoad(input_depth, p, 0).r;
}
@compute @workgroup_size(8, 8)
@@ -12,7 +12,9 @@ struct Params {
};
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var depth: texture_depth_2d;
// A depth view bound as unfilterable float: naga's GLSL backend cannot
// `textureLoad` a depth texture, and the texel is the same value.
@group(0) @binding(1) var depth: texture_2d<f32>;
@group(0) @binding(2) var atlas: texture_2d<f32>;
@group(0) @binding(3) var atlas_sampler: sampler;
@@ -38,7 +40,7 @@ struct Out {
// xy: the motion vector in UV (to where the pixel was last frame, FSR's convention), z: 1.
@fragment fn fs_motion(in: Out) -> @location(0) vec4<f32> {
let stored = textureLoad(depth, vec2<i32>(in.clip.xy), 0);
let stored = textureLoad(depth, vec2<i32>(in.clip.xy), 0).r;
let tolerance = stored * 1e-3 + 1e-7;
// Not what the world shows here: a transparent texel, or behind something.
if in.translucent != 0u {
@@ -16,14 +16,16 @@ const MAPPED: u8 = 3;
const ROW: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
const SHADER: &str = "
@group(0) @binding(0) var depth: texture_depth_2d;
// A depth view bound as unfilterable float: naga's GLSL backend cannot
// `textureLoad` a depth texture, and the texel is the same value.
@group(0) @binding(0) var depth: texture_2d<f32>;
@vertex fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
}
@fragment fn fs() -> @location(0) vec4<f32> {
let centre = vec2<i32>(textureDimensions(depth) / 2u);
return vec4<f32>(textureLoad(depth, centre, 0), 0.0, 0.0, 1.0);
return vec4<f32>(textureLoad(depth, centre, 0).r, 0.0, 0.0, 1.0);
}
";
@@ -64,7 +66,7 @@ impl CenterDepth {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Depth,
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
@@ -433,6 +433,14 @@ struct Mips {
levels: Vec<(wgpu::TextureView, wgpu::BindGroup)>,
}
/// The picture a pack's `final` draws at the world's size while the window is another: it is
/// copied onto the window once, after the pack's passes, as the game's own post pass does
/// without a pack.
struct FinalTarget {
size: [u32; 2],
view: wgpu::TextureView,
}
struct Targets {
size: [u32; 2],
/// A and B of each buffer used.
@@ -664,6 +672,12 @@ pub struct PackPost {
blit_group: Option<([wgpu::TextureView; 2], wgpu::BindGroup)>,
/// Shows colortex0 when the pack has no `final` (OQ-5).
copy: (wgpu::RenderPipeline, wgpu::BindGroupLayout),
/// Copies the picture `final` drew at the world's size onto the window: no flip, it is
/// already in the screen's rows.
upscale: (wgpu::RenderPipeline, wgpu::BindGroupLayout),
/// The picture `final` draws while the world's size is not the window's.
final_target: Option<FinalTarget>,
screen: wgpu::TextureFormat,
uniforms: FrameUniforms,
}
@@ -2724,7 +2738,7 @@ impl PackPost {
},
&[
texture_entry(0, float),
texture_entry(1, wgpu::TextureSampleType::Depth),
texture_entry(1, wgpu::TextureSampleType::Float { filterable: false }),
],
&blit_targets,
// GL's window depth, written for depthtex0 and the terrain's depth test.
@@ -2751,6 +2765,20 @@ impl PackPost {
&[target(screen)],
None,
);
let upscale = helper(
device,
"vs_screen",
"fs_copy",
&[
texture_entry(0, float),
entry(
2,
wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
),
],
&[target(screen)],
None,
);
let filterable = |b: usize| {
formats[b].sample_type(None, Some(device.features()))
== Some(wgpu::TextureSampleType::Float { filterable: true })
@@ -2952,6 +2980,9 @@ impl PackPost {
}),
blit,
copy,
upscale,
final_target: None,
screen,
uniforms: {
let mut u = FrameUniforms::default();
u.atlas = world.atlas.map_or([0; 2], |a| a.1);
@@ -4484,6 +4515,35 @@ impl PackPost {
terrain: &mut TerrainDraw<'_>,
) {
self.resize(device, size);
// A resolution scale leaves the picture smaller (or larger) than the window: the pack's
// `final` draws it at the picture's size, and one copy takes it to the window (#9291).
let final_view = if size != window && self.passes.iter().any(|p| p.group == Group::Final) {
if self.final_target.as_ref().is_none_or(|t| t.size != size) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Shader pack final"),
size: wgpu::Extent3d {
width: size[0],
height: size[1],
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.screen,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
self.final_target = Some(FinalTarget {
size,
view: texture.create_view(&Default::default()),
});
}
self.final_target.as_ref().map(|t| t.view.clone())
} else {
self.final_target = None;
None
};
if let Some((layout, ids, rects)) = &self.block_ids {
// A page that grew has a new buffer.
self.page_groups
@@ -4788,8 +4848,8 @@ impl PackPost {
self.mips_valid |= stale;
let final_pass = pass.group == Group::Final;
// A pass writing buffers of a size of their own draws at that size (they share it).
// `final` draws the picture at the world's size; a copy takes it to the window.
let out = match pass.writes.first() {
_ if final_pass => window,
Some(&(_, b)) => self.targets.as_ref().map_or(size, |t| t.sizes[b]),
None => size,
};
@@ -4812,7 +4872,7 @@ impl PackPost {
overwrite
};
if final_pass {
attachments.push(attach(screen, overwrite));
attachments.push(attach(final_view.as_ref().unwrap_or(screen), overwrite));
shown = true;
} else if pass.sink {
attachments.push(attach(&targets.sink, ops));
@@ -4895,6 +4955,31 @@ impl PackPost {
rp.set_pipeline(&self.copy.0);
rp.set_bind_group(0, &group, &[]);
rp.draw(0..3, 0..1);
} else if let Some(picture) = &final_view {
// The picture at the world's size onto the window: one upscale, smoothly.
let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Shader pack final"),
layout: &self.upscale.1,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(picture),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&self.linear),
},
],
});
let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Shader pack final"),
color_attachments: &[attach(screen, overwrite)],
timestamp_writes: self.timing.as_mut().and_then(|t| t.writes("final copy")),
..Default::default()
});
rp.set_pipeline(&self.upscale.0);
rp.set_bind_group(0, &group, &[]);
rp.draw(0..3, 0..1);
}
}
}
@@ -9,7 +9,9 @@ fn vs_full(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
}
@group(0) @binding(0) var source: texture_2d<f32>;
@group(0) @binding(1) var world_depth: texture_depth_2d;
// A depth view read as an unfilterable float: naga's GLSL backend cannot
// `textureLoad` a depth texture, and the texel is the same either way.
@group(0) @binding(1) var world_depth: texture_2d<f32>;
struct World {
@location(0) color: vec4<f32>,
@@ -24,7 +26,7 @@ fn fs_blit(@builtin(position) p: vec4<f32>) -> World {
let at = vec2<i32>(i32(p.x), size.y - 1 - i32(p.y));
var out: World;
out.color = textureLoad(source, at, 0);
out.depth = 1.0 - textureLoad(world_depth, at, 0);
out.depth = 1.0 - textureLoad(world_depth, at, 0).r;
return out;
}
@@ -49,6 +51,13 @@ fn vs_uv(@builtin(vertex_index) i: u32) -> Full {
return Full(vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0), vec2<f32>(uv.x, 1.0 - uv.y));
}
// The screen's own rows: a picture a shader pack's `final` drew (not a GL-rows buffer).
@vertex
fn vs_screen(@builtin(vertex_index) i: u32) -> Full {
let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
return Full(vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0), uv);
}
@fragment
fn fs_copy(v: Full) -> @location(0) vec4<f32> {
return textureSample(source, linear, v.uv);
@@ -43,6 +43,64 @@ fn a_pack_without_final_reads_the_replaced_world_colour() {
replacement(Change::Colour, false);
}
/// A Resolution Scale leaves the pack's picture smaller than the window. Its `final` draws at
/// the picture's size and one copy upsizes it to the window (#9291), so a final that fetches
/// its pixel by `gl_FragCoord` (as Photon's does) covers every pixel, not a quarter of them.
#[test]
fn a_scaled_picture_fills_the_whole_window() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, queue)) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: adapter.limits(),
..Default::default()
})) else {
return;
};
eprintln!("scaled picture adapter: {:?}", adapter.get_info());
let picture = [4, 2];
let window = [8, 4];
let colour = texture(&device, picture, T::Rgba8Unorm);
let depth = texture(&device, picture, T::Depth32Float);
clear(&device, &queue, &colour, &depth, [32, 64, 16, 255], 0.25);
let mut post = pack_with(
&device,
&queue,
&adapter,
&colour,
&[
(
"final.vsh",
"#version 120\nvoid main(){gl_Position=ftransform();}",
),
(
"final.fsh",
"#version 330 compatibility\nuniform sampler2D colortex0;\nvoid main(){gl_FragColor=texelFetch(colortex0,ivec2(gl_FragCoord.xy),0);}",
),
],
);
let hud = HudBatch::new(window);
let pixels = render(
&device,
&queue,
&mut post,
&colour,
&depth,
&frame(&hud),
picture,
window,
);
// Independently specified: the world's colour at every window pixel.
let wrong = pixels.iter().filter(|p| **p != [32, 64, 16, 255]).count();
assert_eq!(
wrong,
0,
"{wrong} of {} pixels are not the picture",
pixels.len()
);
}
fn replacement(change: Change, final_shader: bool) {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
@@ -62,7 +120,9 @@ fn replacement(change: Change, final_shader: bool) {
let mut post = pack(&device, &queue, &adapter, &colour, final_shader);
let hud = HudBatch::new(size);
let frame = frame(&hud);
let initial = render(&device, &queue, &mut post, &colour, &depth, &frame, size);
let initial = render(
&device, &queue, &mut post, &colour, &depth, &frame, size, size,
);
let expected = if final_shader {
[32, 64, 191, 255]
} else {
@@ -93,9 +153,13 @@ fn replacement(change: Change, final_shader: bool) {
0.75
};
clear(&device, &queue, &colour, &depth, pixel, reverse_depth);
let actual = render(&device, &queue, &mut post, &colour, &depth, &frame, size);
let actual = render(
&device, &queue, &mut post, &colour, &depth, &frame, size, size,
);
let mut fresh = pack(&device, &queue, &adapter, &colour, final_shader);
let reference = render(&device, &queue, &mut fresh, &colour, &depth, &frame, size);
let reference = render(
&device, &queue, &mut fresh, &colour, &depth, &frame, size, size,
);
// Independently specified RG channels and GL depth (1 - reverse-Z), in UNORM8.
let expected = if !final_shader {
[160, 96, 48, 255]
@@ -178,13 +242,8 @@ fn pack(
lightmap: &wgpu::TextureView,
final_shader: bool,
) -> PackPost {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let options = zip::write::SimpleFileOptions::default();
zip.start_file("shaders/shaders.properties", options)
.unwrap();
zip.write_all(b"oldLighting=false\n").unwrap();
if final_shader {
for (name, source) in [
let programs: &[(&str, &str)] = if final_shader {
&[
(
"final.vsh",
"#version 120\nvarying vec2 uv;\nvoid main(){gl_Position=ftransform();uv=gl_MultiTexCoord0.xy;}",
@@ -193,10 +252,29 @@ fn pack(
"final.fsh",
"#version 120\nvarying vec2 uv;uniform sampler2D colortex0;uniform sampler2D depthtex0;\nvoid main(){gl_FragColor=vec4(texture2D(colortex0,uv).rg,texture2D(depthtex0,uv).r,1.0);}",
),
] {
zip.start_file(format!("shaders/{name}"), options).unwrap();
zip.write_all(source.as_bytes()).unwrap();
}
]
} else {
&[]
};
pack_with(device, queue, adapter, lightmap, programs)
}
/// A pack of `programs` (`name`, GLSL source) over `lightmap`.
fn pack_with(
device: &wgpu::Device,
queue: &wgpu::Queue,
adapter: &wgpu::Adapter,
lightmap: &wgpu::TextureView,
programs: &[(&str, &str)],
) -> PackPost {
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
let options = zip::write::SimpleFileOptions::default();
zip.start_file("shaders/shaders.properties", options)
.unwrap();
zip.write_all(b"oldLighting=false\n").unwrap();
for (name, source) in programs {
zip.start_file(format!("shaders/{name}"), options).unwrap();
zip.write_all(source.as_bytes()).unwrap();
}
let pack = horizon_shaders::Pack::from_zip_bytes(
"original-world-inputs",
@@ -241,12 +319,13 @@ fn render(
depth: &wgpu::TextureView,
frame: &Frame,
size: [u32; 2],
window: [u32; 2],
) -> Vec<[u8; 4]> {
let screen = texture(device, size, T::Rgba8Unorm);
let screen = texture(device, window, T::Rgba8Unorm);
let stride = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let read = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: u64::from(stride * size[1]),
size: u64::from(stride * window[1]),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
@@ -261,7 +340,7 @@ fn render(
frame,
&Default::default(),
size,
size,
window,
&[],
&mut |_, _, _, _| {},
);
@@ -285,7 +364,7 @@ fn render(
.unwrap()
.chunks_exact(stride as usize)
.flat_map(|row| {
row[..size[0] as usize * 4]
row[..window[0] as usize * 4]
.as_chunks::<4>()
.0
.iter()
@@ -43,9 +43,7 @@ struct TaaUniform {
jitter: [f32; 4],
}
/// TAA's GPU resources, absent when the device cannot compile the shader
/// (the OpenGL backend's naga GLSL emitter does not support `textureLoad`
/// from depth textures).
/// TAA's GPU resources, absent when the device cannot compile the shader.
struct TaaResources {
layout: wgpu::BindGroupLayout,
pipeline: wgpu::RenderPipeline,
@@ -146,9 +144,6 @@ impl Post {
cache: None,
})
};
// The OpenGL backend's GLSL translator does not support textureLoad
// from depth textures: on devices running through OpenGL (such as older
// Intel GPUs on Linux), TAA cannot compile its pipeline.
let taa_resources = {
let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Horizon TAA bindings"),
@@ -161,7 +156,9 @@ impl Post {
count: None,
},
texture(2, float),
texture(3, wgpu::TextureSampleType::Depth),
// A depth view through an unfilterable float binding: the
// texel is the same, and the GLSL backend can read it.
texture(3, wgpu::TextureSampleType::Float { filterable: false }),
wgpu::BindGroupLayoutEntry {
binding: 4,
visibility: wgpu::ShaderStages::FRAGMENT,
@@ -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"
]
);
}
}
+397 -112
View File
@@ -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;
}
@@ -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]);
}
}
+13 -9
View File
@@ -17,6 +17,7 @@ use horizon_core::{
geometry::{Aabb, Axis, Vec3, trig},
random::{LegacyRandom, RandomSource},
};
use horizon_protocol::entity_data::PositionSource;
use horizon_world::block_collision::BlockCollisionRegistry;
use std::{
array,
@@ -50,9 +51,10 @@ pub struct ParticleOptions {
/// `GeyserParticleOptions.waterBlocks` and `GeyserBaseParticleOptions`'
/// `waterBlocks` and `burstImpulseBase` (0 where the type has none).
pub geyser: Option<(i32, f32)>,
/// `VibrationParticleOption`: the listener the signal flies to and its
/// arrival time in ticks (`ParticleEngine::vibration` is the same particle).
pub vibration: Option<([f64; 3], i32)>,
/// `VibrationParticleOption`: the listener's `PositionSource`, the point
/// the signal flies to and its arrival time in ticks
/// (`ParticleEngine::vibration` is the same particle).
pub vibration: Option<(PositionSource, [f64; 3], i32)>,
/// A native mod's own particle type, by its namespaced name (`mymod:spark`):
/// `SimpleParticleType` plus the provider `particles/<name>.json` names.
pub mod_particle: Option<String>,
@@ -94,10 +96,11 @@ impl ParticleOptions {
..Self::simple(ty)
}
}
/// `VibrationParticleOption(destination, arrivalInTicks)`.
pub fn vibration(dest: [f64; 3], ticks: i32) -> Self {
/// `VibrationParticleOption(destination, arrivalInTicks)`: the listener's
/// position source and the point this engine flies the signal to.
pub fn vibration(source: PositionSource, dest: [f64; 3], ticks: i32) -> Self {
Self {
vibration: Some((dest, ticks)),
vibration: Some((source, dest, ticks)),
..Self::simple(ParticleType::Vibration)
}
}
@@ -2988,8 +2991,8 @@ impl ParticleEngine {
// the listener the signal flies to and its arrival time; the sprite
// is the set's own draw from the engine's random.
T::Vibration => {
let (dest, ticks) = o.vibration?;
let mut q = Self::vibration_particle(p, dest, ticks);
let (_, dest, ticks) = o.vibration.as_ref()?;
let mut q = Self::vibration_particle(p, *dest, *ticks);
q.set = set;
q.sprite = self.sprite_from_set(ty);
q
@@ -5644,7 +5647,8 @@ mod tests {
#[test]
fn a_vibration_option_makes_the_signal_its_provider_draws() {
let mut engine = ParticleEngine::new(Path::new(""), &HashMap::new(), &HashSet::new());
let options = ParticleOptions::vibration([10.5, 70.5, -3.5], 20);
let options =
ParticleOptions::vibration(PositionSource::Block([10, 70, -4]), [10.5, 70.5, -3.5], 20);
// Within the camera's 32 blocks, so only the provider decides.
engine.add_with(&options, [0.5, 1.5, 0.5], [0.0; 3], false, false);
assert_eq!(engine.made().get("vibration"), Some(&1));
+316 -2
View File
@@ -575,8 +575,53 @@ pub struct Hud {
/// Hits in a row the server confirmed, until this player is hurt or 3 seconds pass.
pub combo: bool,
pub combo_corner: Corner,
/// Frames drawn in the last second.
pub fps: bool,
pub fps_corner: Corner,
/// Left and right clicks in the last second.
pub cps: bool,
pub cps_corner: Corner,
/// The movement keys, mouse buttons and jump, lit while held, with clicks per second.
pub keys: bool,
pub keys_corner: Corner,
/// The computer's local time.
pub clock: bool,
pub clock_corner: Corner,
/// How far the last hit landed from the eyes, for 3 seconds; it only measures.
pub reach: bool,
pub reach_corner: Corner,
/// Horizontal speed in blocks per second.
pub speed: bool,
pub speed_corner: Corner,
/// A dark box behind each widget.
pub background: bool,
/// The boxes' opacity, in percent.
pub opacity: u8,
/// The widgets' text colour (durability keeps its own).
pub text: Colour,
/// A shadow under the widgets' text, as vanilla's HUD text has.
pub shadow: bool,
}
impl Hud {
/// Whether any widget is on: with none, the HUD draws nothing and reads nothing.
pub fn any(&self) -> bool {
[
self.armour,
self.effects,
self.held,
self.ping,
self.coords,
self.server,
self.combo,
self.fps,
self.cps,
self.keys,
self.clock,
self.reach,
self.speed,
]
.contains(&true)
}
}
impl Default for Hud {
fn default() -> Self {
@@ -596,11 +641,35 @@ impl Default for Hud {
server_corner: Corner::TopLeft,
combo: false,
combo_corner: Corner::TopLeft,
fps: false,
fps_corner: Corner::TopLeft,
cps: false,
cps_corner: Corner::TopLeft,
keys: false,
keys_corner: Corner::BottomLeft,
clock: false,
clock_corner: Corner::TopRight,
reach: false,
reach_corner: Corner::TopLeft,
speed: false,
speed_corner: Corner::TopLeft,
background: true,
opacity: HUD_OPACITY,
text: Colour::White,
shadow: true,
}
}
}
pub use crate::extras::Corner;
/// The widgets' boxes at first: the extras' 0x78 alpha, 47%.
pub const HUD_OPACITY: u8 = 47;
pub const HUD_OPACITIES: (u8, u8) = (0, 100);
impl Hud {
/// The boxes' colour, ARGB.
pub fn background_argb(&self) -> u32 {
alpha(self.opacity) << 24
}
}
/// The value of the view options while they follow the world.
fn world_label() -> String {
crate::lang::get_or("horizon.pvp.view.world", "World's")
@@ -682,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,
@@ -690,6 +790,28 @@ pub struct Pvp {
pub hud: Hud,
pub tweaks: Tweaks,
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 {
@@ -743,6 +865,15 @@ impl Pvp {
"hudHeld" => u.held = flag(value, u.held),
"hudHeldCorner" => u.held_corner = Corner::parse(value, u.held_corner),
"hudBackground" => u.background = flag(value, u.background),
"hudOpacity" => u.opacity = num(value, HUD_OPACITIES, u.opacity),
"hudTextColour" => {
u.text = Colour::parse(value)
.filter(|c| *c != Colour::Inverted)
.unwrap_or(u.text)
}
"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),
@@ -751,6 +882,18 @@ impl Pvp {
"hudServerCorner" => u.server_corner = Corner::parse(value, u.server_corner),
"hudCombo" => u.combo = flag(value, u.combo),
"hudComboCorner" => u.combo_corner = Corner::parse(value, u.combo_corner),
"hudFps" => u.fps = flag(value, u.fps),
"hudFpsCorner" => u.fps_corner = Corner::parse(value, u.fps_corner),
"hudCps" => u.cps = flag(value, u.cps),
"hudCpsCorner" => u.cps_corner = Corner::parse(value, u.cps_corner),
"hudKeys" => u.keys = flag(value, u.keys),
"hudKeysCorner" => u.keys_corner = Corner::parse(value, u.keys_corner),
"hudClock" => u.clock = flag(value, u.clock),
"hudClockCorner" => u.clock_corner = Corner::parse(value, u.clock_corner),
"hudReach" => u.reach = flag(value, u.reach),
"hudReachCorner" => u.reach_corner = Corner::parse(value, u.reach_corner),
"hudSpeed" => u.speed = flag(value, u.speed),
"hudSpeedCorner" => u.speed_corner = Corner::parse(value, u.speed_corner),
"numericEnchants" => t.numeric_enchants = flag(value, t.numeric_enchants),
"lowFire" => t.fire_lower = num(value, FIRE_LOWER, t.fire_lower),
"shieldSize" => t.shield_size = num(value, SHIELD_SIZES, t.shield_size),
@@ -767,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,
@@ -838,6 +989,35 @@ impl Pvp {
u.combo,
u.combo_corner.name(),
);
let _ = write!(
out,
"hudFps:{}\nhudFpsCorner:{}\nhudCps:{}\nhudCpsCorner:{}\nhudKeys:{}\nhudKeysCorner:{}\n",
u.fps,
u.fps_corner.name(),
u.cps,
u.cps_corner.name(),
u.keys,
u.keys_corner.name(),
);
let _ = write!(
out,
"hudClock:{}\nhudClockCorner:{}\nhudReach:{}\nhudReachCorner:{}\nhudSpeed:{}\nhudSpeedCorner:{}\n",
u.clock,
u.clock_corner.name(),
u.reach,
u.reach_corner.name(),
u.speed,
u.speed_corner.name(),
);
let _ = write!(
out,
"hudOpacity:{}\nhudTextColour:{}\nhudShadow:{}\n",
u.opacity,
u.text.name(),
u.shadow,
);
self.layout.serialize(out);
let _ = writeln!(out, "hudEditorKey:{}", self.editor_key);
let t = &self.tweaks;
let _ = write!(
out,
@@ -862,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
);
}
}
@@ -955,7 +1142,27 @@ opts! {
HudCombo, "hud_combo", "Combo counter",
"Hits in a row the server confirmed, until you are hurt or 3 seconds pass without one.";
HudComboCorner, "hud_combo_corner", "Combo corner", "Where the combo shows.";
HudFps, "hud_fps", "FPS", "Frames drawn in the last second.";
HudFpsCorner, "hud_fps_corner", "FPS corner", "Where the FPS shows.";
HudCps, "hud_cps", "Clicks per second", "Your left and right clicks in the last second.";
HudCpsCorner, "hud_cps_corner", "CPS corner", "Where the clicks per second show.";
HudKeys, "hud_keys", "Keystrokes",
"Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.";
HudKeysCorner, "hud_keys_corner", "Keystrokes corner", "Where the keystrokes show.";
HudClock, "hud_clock", "Clock", "Your computer's time.";
HudClockCorner, "hud_clock_corner", "Clock corner", "Where the clock shows.";
HudReach, "hud_reach", "Reach",
"How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.";
HudReachCorner, "hud_reach_corner", "Reach corner", "Where the reach shows.";
HudSpeed, "hud_speed", "Speed", "How fast you move across the ground, in blocks per second.";
HudSpeedCorner, "hud_speed_corner", "Speed corner", "Where the speed shows.";
HudBackground, "hud_background", "Widget backgrounds", "A dark box behind each widget.";
HudOpacity, "hud_opacity", "Background opacity", "How dark the boxes behind the widgets are.";
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",
@@ -980,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>> {
@@ -1030,7 +1245,23 @@ pub fn rows() -> Vec<Result<Opt, &'static str>> {
HudServerCorner,
HudCombo,
HudComboCorner,
HudFps,
HudFpsCorner,
HudCps,
HudCpsCorner,
HudKeys,
HudKeysCorner,
HudClock,
HudClockCorner,
HudReach,
HudReachCorner,
HudSpeed,
HudSpeedCorner,
HudBackground,
HudOpacity,
HudTextColour,
HudShadow,
HudEditorKey,
]
.map(Ok),
);
@@ -1043,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 {
@@ -1054,10 +1287,19 @@ impl Opt {
OutlineColour | OutlineOpacity | OutlineThickness | OutlineFill => Some(Outline),
HitColour | HitOpacity => Some(Hit),
HudArmour | HudEffects | HudHeld | HudPing | HudCoords | HudServer | HudCombo
| HudBackground | NumericEnchants => None,
| HudFps | HudCps | HudKeys | HudClock | HudReach | HudSpeed | HudBackground
| HudTextColour | HudShadow | HudEditorKey | NumericEnchants => None,
HudOpacity => Some(HudBackground),
HudFpsCorner => Some(HudFps),
HudCpsCorner => Some(HudCps),
HudKeysCorner => Some(HudKeys),
HudClockCorner => Some(HudClock),
HudReachCorner => Some(HudReach),
HudSpeedCorner => Some(HudSpeed),
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),
@@ -1081,10 +1323,17 @@ impl Opt {
Opt::HudEffects => p.hud.effects,
Opt::HudHeld => p.hud.held,
Opt::HudBackground => p.hud.background,
Opt::HudShadow => p.hud.shadow,
Opt::HudPing => p.hud.ping,
Opt::HudCoords => p.hud.coords,
Opt::HudServer => p.hud.server,
Opt::HudCombo => p.hud.combo,
Opt::HudFps => p.hud.fps,
Opt::HudCps => p.hud.cps,
Opt::HudKeys => p.hud.keys,
Opt::HudClock => p.hud.clock,
Opt::HudReach => p.hud.reach,
Opt::HudSpeed => p.hud.speed,
Opt::NumericEnchants => p.tweaks.numeric_enchants,
Opt::ClearWater => p.tweaks.clear_water,
Opt::ClearLava => p.tweaks.clear_lava,
@@ -1092,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,
})
}
@@ -1124,6 +1376,15 @@ impl Opt {
Opt::HudCoordsCorner => corner_label(p.hud.coords_corner),
Opt::HudServerCorner => corner_label(p.hud.server_corner),
Opt::HudComboCorner => corner_label(p.hud.combo_corner),
Opt::HudFpsCorner => corner_label(p.hud.fps_corner),
Opt::HudCpsCorner => corner_label(p.hud.cps_corner),
Opt::HudKeysCorner => corner_label(p.hud.keys_corner),
Opt::HudClockCorner => corner_label(p.hud.clock_corner),
Opt::HudReachCorner => corner_label(p.hud.reach_corner),
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}%"),
@@ -1134,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
@@ -1191,6 +1456,11 @@ impl Opt {
Opt::HudHeld => u.held = !u.held,
Opt::HudHeldCorner => u.held_corner = corner(u.held_corner),
Opt::HudBackground => u.background = !u.background,
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,
@@ -1199,6 +1469,18 @@ impl Opt {
Opt::HudServerCorner => u.server_corner = corner(u.server_corner),
Opt::HudCombo => u.combo = !u.combo,
Opt::HudComboCorner => u.combo_corner = corner(u.combo_corner),
Opt::HudFps => u.fps = !u.fps,
Opt::HudFpsCorner => u.fps_corner = corner(u.fps_corner),
Opt::HudCps => u.cps = !u.cps,
Opt::HudCpsCorner => u.cps_corner = corner(u.cps_corner),
Opt::HudKeys => u.keys = !u.keys,
Opt::HudKeysCorner => u.keys_corner = corner(u.keys_corner),
Opt::HudClock => u.clock = !u.clock,
Opt::HudClockCorner => u.clock_corner = corner(u.clock_corner),
Opt::HudReach => u.reach = !u.reach,
Opt::HudReachCorner => u.reach_corner = corner(u.reach_corner),
Opt::HudSpeed => u.speed = !u.speed,
Opt::HudSpeedCorner => u.speed_corner = corner(u.speed_corner),
Opt::NumericEnchants => p.tweaks.numeric_enchants = !p.tweaks.numeric_enchants,
Opt::FireLower => t.fire_lower = step_by(t.fire_lower, FIRE_LOWER, 10, back),
Opt::ShieldSize => t.shield_size = step_by(t.shield_size, SHIELD_SIZES, 10, back),
@@ -1216,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))
@@ -1518,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));
}
}
+451
View File
@@ -214,6 +214,320 @@ impl Combo {
}
}
/// Clicks per second: the presses of the last second, kept for the widgets that are on.
#[derive(Default)]
pub struct Clicks {
/// Press times in seconds, oldest first; at most [`Clicks::KEEP`].
times: std::collections::VecDeque<f64>,
}
impl Clicks {
/// More than any hand clicks in a second: the ring never grows past it.
const KEEP: usize = 64;
pub fn press(&mut self, now: f64) {
if self.times.len() == Self::KEEP {
self.times.pop_front();
}
self.times.push_back(now);
}
/// The presses in the second up to `now`.
pub fn per_second(&mut self, now: f64) -> u32 {
while self.times.front().is_some_and(|&t| now - t >= 1.0) {
self.times.pop_front();
}
self.times.len() as u32
}
}
/// Horizontal speed in blocks per second, from the player's own position sampled at most
/// every [`Speed::SAMPLE`] seconds (nothing is asked of the server).
#[derive(Default)]
pub struct Speed {
last: Option<([f64; 2], f64)>,
/// Tenths of a block per second.
tenths: u32,
}
impl Speed {
pub const SAMPLE: f64 = 0.25;
pub fn sample(&mut self, x: f64, z: f64, now: f64) -> u32 {
match self.last {
Some((_, at)) if now - at < Self::SAMPLE => {}
Some(([lx, lz], at)) => {
let d = ((x - lx).powi(2) + (z - lz).powi(2)).sqrt();
// A teleport or respawn isn't speed.
let v = d / (now - at);
self.tenths = if v > 200.0 {
0
} else {
(v * 10.0).round() as u32
};
self.last = Some(([x, z], now));
}
None => self.last = Some(([x, z], now)),
}
self.tenths
}
}
/// The reach display: how far the last hit this player made was, measured from the eyes to
/// the point the crosshair's ray met the target's box. It measures what the client already
/// computed for the attack and changes nothing about it.
#[derive(Default)]
pub struct Reach {
last: Option<(f64, f64)>,
}
impl Reach {
/// How long the last hit's distance stays shown.
pub const SHOWN: f64 = 3.0;
pub fn hit(&mut self, distance: f64, now: f64) {
self.last = Some((distance, now));
}
/// Hundredths of a block, while the last hit is recent.
pub fn hundredths(&self, now: f64) -> Option<u32> {
let (d, at) = self.last?;
(now - at <= Self::SHOWN).then(|| (d * 100.0).round() as u32)
}
}
/// `n` hundredths as "3.05".
pub fn hundredths(n: u32) -> String {
format!("{}.{:02}", n / 100, n % 100)
}
/// `n` tenths as "4.3".
pub fn tenths(n: u32) -> String {
format!("{}.{}", n / 10, n % 10)
}
/// The local time as "14:05".
pub fn clock(hour: u8, minute: u8) -> String {
format!("{hour:02}:{minute:02}")
}
/// The keystrokes widget's keys, in drawing order.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Key {
Forward,
Left,
Back,
Right,
Attack,
Use,
Jump,
}
impl Key {
pub const ALL: [Key; 7] = [
Key::Forward,
Key::Left,
Key::Back,
Key::Right,
Key::Attack,
Key::Use,
Key::Jump,
];
/// The key mapping it shows.
pub fn mapping(self) -> &'static str {
match self {
Key::Forward => "key.forward",
Key::Left => "key.left",
Key::Back => "key.back",
Key::Right => "key.right",
Key::Attack => "key.attack",
Key::Use => "key.use",
Key::Jump => "key.jump",
}
}
}
/// The keystrokes' key size and the gap between keys, in GUI pixels.
pub const KEY: f32 = 22.0;
pub const KEY_GAP: f32 = 2.0;
/// The space bar's height.
pub const SPACE: f32 = 12.0;
/// Where each key of the keystrokes widget sits in its box: W above A S D, the two mouse
/// buttons under them, the space bar at the bottom, as `(x, y, w, h)`.
pub fn key_rect(key: Key) -> (f32, f32, f32, f32) {
let step = KEY + KEY_GAP;
let full = 3.0 * KEY + 2.0 * KEY_GAP;
let half = (full - KEY_GAP) / 2.0;
match key {
Key::Forward => (step, 0.0, KEY, KEY),
Key::Left => (0.0, step, KEY, KEY),
Key::Back => (step, step, KEY, KEY),
Key::Right => (2.0 * step, step, KEY, KEY),
Key::Attack => (0.0, 2.0 * step, half, KEY),
Key::Use => (half + KEY_GAP, 2.0 * step, half, KEY),
Key::Jump => (0.0, 3.0 * step, full, SPACE),
}
}
/// The keystrokes widget's size.
pub fn keys_size() -> (f32, f32) {
let (_, y, w, h) = key_rect(Key::Jump);
(w, y + h)
}
/// The HUD widgets, for their places on screen ([`Layout`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetId {
Armour,
Effects,
Held,
Ping,
Coords,
Server,
Combo,
Fps,
Cps,
Keys,
Clock,
Reach,
Speed,
}
impl WidgetId {
pub const ALL: [WidgetId; 13] = [
WidgetId::Armour,
WidgetId::Effects,
WidgetId::Held,
WidgetId::Ping,
WidgetId::Coords,
WidgetId::Server,
WidgetId::Combo,
WidgetId::Fps,
WidgetId::Cps,
WidgetId::Keys,
WidgetId::Clock,
WidgetId::Reach,
WidgetId::Speed,
];
pub fn name(self) -> &'static str {
match self {
WidgetId::Armour => "armour",
WidgetId::Effects => "effects",
WidgetId::Held => "held",
WidgetId::Ping => "ping",
WidgetId::Coords => "coords",
WidgetId::Server => "server",
WidgetId::Combo => "combo",
WidgetId::Fps => "fps",
WidgetId::Cps => "cps",
WidgetId::Keys => "keys",
WidgetId::Clock => "clock",
WidgetId::Reach => "reach",
WidgetId::Speed => "speed",
}
}
pub fn index(self) -> usize {
self as usize
}
}
/// Where a widget sits: in its corner, stacked with the others there (`at` is `None`), or at a
/// place the player dragged it to, as fractions of the room the screen leaves around the box
/// (0 at the left or top edge, 1 at the right or bottom), so it stays on screen at any size.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Place {
pub at: Option<(f32, f32)>,
/// Its size in percent.
pub scale: u8,
}
impl Default for Place {
fn default() -> Self {
Self {
at: None,
scale: 100,
}
}
}
pub const SCALES: (u8, u8) = (50, 200);
/// Every widget's place.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Layout(pub [Place; 13]);
impl Layout {
pub fn get(&self, id: WidgetId) -> Place {
self.0[id.index()]
}
pub fn get_mut(&mut self, id: WidgetId) -> &mut Place {
&mut self.0[id.index()]
}
/// One `hudPlace` value: `armour,0.25,0.5,150`, or `armour,corner,,150`.
pub fn parse(&mut self, value: &str) {
let mut parts = value.split(',');
let Some(id) = parts
.next()
.and_then(|n| WidgetId::ALL.into_iter().find(|w| w.name() == n))
else {
return;
};
let (x, y, scale) = (parts.next(), parts.next(), parts.next());
let frac = |v: Option<&str>| {
v.and_then(|v| v.parse::<f32>().ok())
.filter(|v| v.is_finite())
.map(|v| v.clamp(0.0, 1.0))
};
let place = self.get_mut(id);
place.at = frac(x).zip(frac(y));
if let Some(s) = scale.and_then(|s| s.parse::<u8>().ok()) {
place.scale = s.clamp(SCALES.0, SCALES.1);
}
}
/// The `hudPlace` lines of the widgets that left their corner or their size.
pub fn serialize(&self, out: &mut String) {
use std::fmt::Write;
for id in WidgetId::ALL {
let p = self.get(id);
if p == Place::default() {
continue;
}
let _ = match p.at {
Some((x, y)) => writeln!(out, "hudPlace:{},{x:.4},{y:.4},{}", id.name(), p.scale),
None => writeln!(out, "hudPlace:{},corner,,{}", id.name(), p.scale),
};
}
}
}
/// A box `w` x `h` placed at fractions `(fx, fy)` of the room left on a `screen`.
pub fn from_fractions((fx, fy): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
(
(fx * (screen.0 - w).max(0.0)).round(),
(fy * (screen.1 - h).max(0.0)).round(),
)
}
/// The fractions that put a `w` x `h` box at `(x, y)` on a `screen`, kept on screen.
pub fn to_fractions((x, y): (f32, f32), (w, h): (f32, f32), screen: (f32, f32)) -> (f32, f32) {
let frac = |v: f32, room: f32| {
if room > 0.0 {
(v / room).clamp(0.0, 1.0)
} else {
0.0
}
};
(frac(x, screen.0 - w), frac(y, screen.1 - h))
}
/// How near, in GUI pixels, an edge must come to another to snap to it.
pub const SNAP: f32 = 4.0;
/// Snaps a box's start `x` of length `w` along one axis of length `screen`: to the screen's
/// edges and middle, and to the edges of the `others` boxes (`(start, length)`), where one
/// is within [`SNAP`]. Returns the new start and the line it snapped to, if any.
pub fn snap(x: f32, w: f32, screen: f32, others: &[(f32, f32)]) -> (f32, Option<f32>) {
let mut best: Option<(f32, f32, f32)> = None;
let mut consider = |start: f32, line: f32| {
let d = (start - x).abs();
if d <= SNAP && best.is_none_or(|(b, _, _)| d < b) {
best = Some((d, start, line));
}
};
// The screen's edges, with the corners' margin, and its middle.
consider(2.0, 0.0);
consider(screen - 2.0 - w, screen);
consider((screen - w) / 2.0, screen / 2.0);
for &(o, ow) in others {
// Side by side, or lined up on the same edge.
consider(o + ow, o + ow);
consider(o - w, o);
consider(o, o);
consider(o + ow - w, o + ow);
}
match best {
Some((_, start, line)) => (start.clamp(0.0, (screen - w).max(0.0)), Some(line)),
None => (x.clamp(0.0, (screen - w).max(0.0)), None),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -338,4 +652,141 @@ mod tests {
assert_eq!(h, 18.0 + 10.0 + 20.0 + 2.0 * PAD - 2.0);
assert!(size(&[]) == (0.0, 0.0));
}
#[test]
fn clicks_count_the_presses_of_the_last_second() {
let mut c = Clicks::default();
for t in [0.0, 0.1, 0.5, 0.9] {
c.press(t);
}
assert_eq!(c.per_second(0.95), 4);
// 1.0 s after the first press it has left the window.
assert_eq!(c.per_second(1.0), 3);
assert_eq!(c.per_second(1.6), 1);
assert_eq!(c.per_second(5.0), 0);
// The ring never keeps more than 64 presses.
for i in 0..200 {
c.press(10.0 + f64::from(i) * 0.001);
}
assert_eq!(c.per_second(10.2), 64);
}
#[test]
fn speed_is_horizontal_blocks_per_second_between_samples() {
let mut s = Speed::default();
assert_eq!(s.sample(0.0, 0.0, 0.0), 0);
// Sprinting: 5.612 blocks per second along x, with a jump (y is ignored).
assert_eq!(s.sample(0.5, 0.0, 0.1), 0, "too soon for a new sample");
assert_eq!(s.sample(1.403, 0.0, 0.25), 56);
// 3 east and 4 south in a second: 5 blocks per second.
assert_eq!(s.sample(1.403 + 3.0, 4.0, 1.25), 50);
// A teleport reads as standing still.
assert_eq!(s.sample(1000.0, 4.0, 1.5), 0);
assert_eq!(tenths(56), "5.6");
}
#[test]
fn reach_shows_the_last_hit_for_three_seconds() {
let mut r = Reach::default();
assert_eq!(r.hundredths(0.0), None);
r.hit(2.954, 1.0);
assert_eq!(r.hundredths(1.5), Some(295));
assert_eq!(hundredths(295), "2.95");
assert_eq!(hundredths(300), "3.00");
assert_eq!(r.hundredths(4.1), None);
}
#[test]
fn the_clock_reads_hours_and_minutes() {
assert_eq!(clock(14, 5), "14:05");
assert_eq!(clock(0, 0), "00:00");
}
#[test]
fn keystrokes_keys_tile_their_box_without_overlap() {
let (w, h) = keys_size();
assert_eq!((w, h), (70.0, 3.0 * 24.0 + 12.0));
for (i, a) in Key::ALL.iter().enumerate() {
let (x, y, kw, kh) = key_rect(*a);
assert!(
x >= 0.0 && y >= 0.0 && x + kw <= w && y + kh <= h,
"{a:?} inside"
);
for b in &Key::ALL[i + 1..] {
let (bx, by, bw, bh) = key_rect(*b);
let apart = x + kw <= bx || bx + bw <= x || y + kh <= by || by + bh <= y;
assert!(apart, "{a:?} and {b:?} overlap");
}
}
}
#[test]
fn places_round_trip_and_only_moved_widgets_are_written() {
let mut l = Layout::default();
let mut text = String::new();
l.serialize(&mut text);
assert_eq!(text, "", "a vanilla layout writes nothing");
*l.get_mut(WidgetId::Keys) = Place {
at: Some((0.25, 1.0)),
scale: 150,
};
l.get_mut(WidgetId::Fps).scale = 75;
l.serialize(&mut text);
assert_eq!(
text,
"hudPlace:fps,corner,,75\nhudPlace:keys,0.2500,1.0000,150\n"
);
let mut back = Layout::default();
for line in text.lines() {
back.parse(line.split_once(':').unwrap().1);
}
assert_eq!(back, l);
// Out of range values are clamped, unknown widgets and bad numbers ignored.
back.parse("speed,-3,7,250");
assert_eq!(back.get(WidgetId::Speed).at, Some((0.0, 1.0)));
assert_eq!(back.get(WidgetId::Speed).scale, 200);
back.parse("radar,0.5,0.5,100");
back.parse("clock,NaN,0.5,100");
assert_eq!(back.get(WidgetId::Clock).at, None);
}
#[test]
fn fractions_keep_a_box_on_screen_at_any_size() {
let screen = (480.0, 270.0);
let at = to_fractions((100.0, 50.0), (80.0, 20.0), screen);
assert_eq!(from_fractions(at, (80.0, 20.0), screen), (100.0, 50.0));
// A box at the right edge stays at the right edge on a wider screen.
let right = to_fractions((400.0, 0.0), (80.0, 20.0), screen);
assert_eq!(right, (1.0, 0.0));
assert_eq!(
from_fractions(right, (80.0, 20.0), (960.0, 540.0)),
(880.0, 0.0)
);
// Dragged past the edge: kept inside.
assert_eq!(
to_fractions((-30.0, 900.0), (80.0, 20.0), screen),
(0.0, 1.0)
);
}
#[test]
fn boxes_snap_to_screen_edges_middle_and_each_other() {
// Near the left margin.
assert_eq!(snap(4.5, 50.0, 480.0, &[]), (2.0, Some(0.0)));
// Centred: the middle of 480 for a 50-wide box starts at 215.
assert_eq!(snap(213.0, 50.0, 480.0, &[]), (215.0, Some(240.0)));
// Beside another box (100..160): its start goes to 160.
assert_eq!(
snap(163.0, 50.0, 480.0, &[(100.0, 60.0)]),
(160.0, Some(160.0))
);
// Lined up with its left edge.
assert_eq!(
snap(98.0, 50.0, 480.0, &[(100.0, 60.0)]),
(100.0, Some(100.0))
);
// Too far from anything: stays, but never off screen.
assert_eq!(snap(300.0, 50.0, 480.0, &[(100.0, 60.0)]), (300.0, None));
assert_eq!(snap(470.0, 50.0, 480.0, &[]), (430.0, None));
}
}
@@ -814,6 +814,7 @@ pub enum Opt {
PadToggleSneak,
PadMoveDeadzone,
PadLookDeadzone,
PadTriggerThreshold,
PadGlyphs,
PadKeyboard,
/// `options.modelPart.*`: one skin layer (index into `options_file::MODEL_PARTS`).
@@ -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);
+43 -1
View File
@@ -614,7 +614,12 @@ fn button_with_room(
room: f32,
narration: impl FnOnce([f32; 4]) -> crate::widgets::Narratable,
) {
crate::widgets::record(&label.plain(), [x, y, w, h], active);
let plain = label.plain();
crate::widgets::record(&plain, [x, y, w, h], active);
// An icon button has no label to record, and A presses it all the same.
if active && plain.trim().is_empty() {
crate::widgets::target([x, y, w, h]);
}
crate::widgets::narratable(|| narration([x, y, w, h]).hovered(hover));
crate::widgets::widget_cursor(hover, active);
let sprite = if !active {
@@ -1846,6 +1851,43 @@ mod tests {
assert_eq!(scrolled_left(10.0, 40.0, 1_000), 10.0 - offset);
}
/// A controller reaches an icon button only through its target, and can't press a disabled one.
#[test]
fn an_active_icon_button_is_a_focus_target_and_a_disabled_one_is_not() {
let atlas = atlas();
let mut batch = HudBatch::new([640, 480]);
// Whatever this thread drew before is the last frame's, then gone.
crate::widgets::end_frame(u64::MAX - 1);
button(
&mut batch,
&atlas,
10.0,
20.0,
20.0,
20.0,
&Text::empty(),
true,
false,
);
button(
&mut batch,
&atlas,
40.0,
20.0,
20.0,
20.0,
&Text::empty(),
false,
false,
);
crate::widgets::end_frame(u64::MAX);
let targets: Vec<[f32; 4]> = crate::widgets::last_targets()
.iter()
.map(|t| t.rect)
.collect();
assert_eq!(targets, [[10.0, 20.0, 20.0, 20.0]]);
}
#[test]
fn a_glyph_leaves_a_label_that_fits_centred() {
let atlas = atlas();
+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());
}
}
@@ -65,7 +65,11 @@ struct Taa {
jitter: vec4<f32>,
};
@group(0) @binding(2) var history: texture_2d<f32>;
@group(0) @binding(3) var depth: texture_depth_2d;
// The world's depth as an unfilterable float texture: naga's GLSL backend
// cannot emit `textureLoad` from a depth texture, and a depth view bound as
// `TextureSampleType::Float { filterable: false }` gives the same texel on
// every backend.
@group(0) @binding(3) var depth: texture_2d<f32>;
@group(0) @binding(4) var<uniform> taa: Taa;
// Catmull-Rom over the history in 5 bilinear taps: sharper than one under motion.
@@ -113,7 +117,7 @@ fn sample_history(uv: vec2<f32>) -> vec3<f32> {
let c = textureLoad(world, q, 0).rgb;
lo = min(lo, c);
hi = max(hi, c);
near = max(near, textureLoad(depth, q, 0));
near = max(near, textureLoad(depth, q, 0).r);
}
}
let ndc = vec2(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0);
+2
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"] }
@@ -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",
@@ -357,10 +357,14 @@
"horizon.extras.header.cosmetics": "Kosmetik",
"horizon.extras.cosmetics_mine": "Meine Kosmetik",
"horizon.extras.cosmetics_mine.tooltip": "Ausgeblendet: Alle Horizon-Spieler sehen dich ohne deine Kosmetik, du selbst auch. Derselbe Schalter wie in der Garderobe des Launchers.",
"key.emote": "Emote-Rad",
"horizon.emote_wheel.title": "Emotes",
"horizon.emote_wheel.empty": "Noch keine Emotes",
"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",
@@ -557,10 +580,14 @@
"horizon.extras.header.cosmetics": "Cosmetics",
"horizon.extras.cosmetics_mine": "My cosmetics",
"horizon.extras.cosmetics_mine.tooltip": "Hidden: every Horizon player sees you without your cosmetics, you included. The same switch as the launcher's Wardrobe.",
"key.emote": "Emote Wheel",
"horizon.emote_wheel.title": "Emotes",
"horizon.emote_wheel.empty": "No emotes yet",
"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",
@@ -676,6 +703,12 @@
"horizon.pvp.hud_held_corner.tooltip": "Where the count shows.",
"horizon.pvp.hud_background": "Widget backgrounds",
"horizon.pvp.hud_background.tooltip": "A dark box behind each widget.",
"horizon.pvp.hud_opacity": "Background opacity",
"horizon.pvp.hud_opacity.tooltip": "How dark the boxes behind the widgets are.",
"horizon.pvp.hud_text_colour": "Text colour",
"horizon.pvp.hud_text_colour.tooltip": "The widgets' text colour. Durability keeps its green to red.",
"horizon.pvp.hud_shadow": "Text shadow",
"horizon.pvp.hud_shadow.tooltip": "A shadow under the widgets' text, as vanilla's HUD text has.",
"horizon.pvp.header.hud": "HUD widgets",
"horizon.pvp.corner.top_left": "Top left",
"horizon.pvp.corner.top_right": "Top right",
@@ -705,6 +738,37 @@
"horizon.pvp.hud.direction.south": "South",
"horizon.pvp.hud.direction.east": "East",
"horizon.pvp.hud.direction.west": "West",
"horizon.pvp.hud_fps": "FPS",
"horizon.pvp.hud_fps.tooltip": "Frames drawn in the last second.",
"horizon.pvp.hud_fps_corner": "FPS corner",
"horizon.pvp.hud_fps_corner.tooltip": "Where the FPS shows.",
"horizon.pvp.hud_cps": "Clicks per second",
"horizon.pvp.hud_cps.tooltip": "Your left and right clicks in the last second.",
"horizon.pvp.hud_cps_corner": "CPS corner",
"horizon.pvp.hud_cps_corner.tooltip": "Where the clicks per second show.",
"horizon.pvp.hud_keys": "Keystrokes",
"horizon.pvp.hud_keys.tooltip": "Your movement keys, mouse buttons and jump, lit while you hold them, with clicks per second.",
"horizon.pvp.hud_keys_corner": "Keystrokes corner",
"horizon.pvp.hud_keys_corner.tooltip": "Where the keystrokes show.",
"horizon.pvp.hud_clock": "Clock",
"horizon.pvp.hud_clock.tooltip": "Your computer's time.",
"horizon.pvp.hud_clock_corner": "Clock corner",
"horizon.pvp.hud_clock_corner.tooltip": "Where the clock shows.",
"horizon.pvp.hud_reach": "Reach",
"horizon.pvp.hud_reach.tooltip": "How far from your eyes your last hit landed, for 3 seconds. It only measures: your reach stays vanilla's.",
"horizon.pvp.hud_reach_corner": "Reach corner",
"horizon.pvp.hud_reach_corner.tooltip": "Where the reach shows.",
"horizon.pvp.hud_speed": "Speed",
"horizon.pvp.hud_speed.tooltip": "How fast you move across the ground, in blocks per second.",
"horizon.pvp.hud_speed_corner": "Speed corner",
"horizon.pvp.hud_speed_corner.tooltip": "Where the speed shows.",
"horizon.pvp.hud.fps": "%s FPS",
"horizon.pvp.hud.cps": "CPS: %s | %s",
"horizon.pvp.hud.reach": "Reach: %s",
"horizon.pvp.hud.speed": "Speed: %s b/s",
"horizon.pvp.hud.keys.attack": "LMB",
"horizon.pvp.hud.keys.use": "RMB",
"horizon.pvp.hud.keys.cps": "%s CPS",
"horizon.pvp.numeric_enchants": "Enchantment levels as numbers",
"horizon.pvp.numeric_enchants.tooltip": "Tooltips and the enchanting table show Sharpness 5 instead of Sharpness V.",
"horizon.pvp.header.items": "Items",
@@ -749,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",
@@ -771,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",
@@ -394,10 +416,14 @@
"horizon.extras.header.cosmetics": "Cosmétiques",
"horizon.extras.cosmetics_mine": "Mes cosmétiques",
"horizon.extras.cosmetics_mine.tooltip": "Masqués : tous les joueurs Horizon vous voient sans vos cosmétiques, vous compris. Le même réglage que la garde-robe du launcher.",
"key.emote": "Roue des emotes",
"horizon.emote_wheel.title": "Emotes",
"horizon.emote_wheel.empty": "Pas encore d'emote",
"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",
@@ -432,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"
Loaded 100 of 488 files, more files were not shown because too many files have changed in this diff. Show more