Once the search field had the focus it kept the keyboard, so Enter did nothing and letters filtered
the list with the ring on a row. Moving the focus away now ends typing.
A key that moves nothing shows no ring, a row greyed out while focused does nothing, the wheel stops
behind a resource error as `scroll_at` does, and a description too long for the strip ends in an
ellipsis, with room for one more line on Extras.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A screen now implements settings_shell::Screen, describing its pages, groups,
rows and values, and Shell does the drawing, clicks, scrolling, the controller's
focus and declared buttons, keys and each row's description. OptionsUi reaches
it through one accessor, so the wheel and a stick's scrolling can't part ways.
Horizon Extras is the first screen on it. Its settings do what they did and are
saved the same way. It gains the shell's look and search, and its descriptions
move from tooltips to the strip, which grows for them.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its rows and buttons only painted, so a pad reached them through the crosshair alone. The D-pad now
walks the recordings to the buttons, the right stick scrolls them, and Done declares its button.
A notch is now one whole row, since nothing clips a row that's partly scrolled off, and the scroll
limit counts the rows that fit, so the last recording can be reached with the mouse too.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Play in Background, off by default and per player, lets the pad keep driving the game while its
window has no focus, and a game the pad is playing doesn't pause on losing focus.
While several play, the router now plays each player's rumble on that player's pad. Rumble no
longer stops when the window loses focus: nothing asked for that, and it's useful to an AFK player.
Starting or leaving splitscreen builds a new renderer, whose atlas has none of what was drawn into
it at run time. Those two paths only forgot the skins, so the on-screen keyboard lost its letters
and rounded corners, and maps were exposed the same way. All renderer swaps now forget the same
things.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Take back the server list's move arrows as targets: they split Tab's stop on
the list, and the D-pad still couldn't land on them.
- A right stick on Z takes over in every direction, not only its pulled end.
- A pad's toggle-sneak doesn't count as holding, or a rebound key's release could
leave the player sneaking.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Releasing Shift while R3 held sneak in hold mode dropped it for both, and so did
the other way round.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Each was painted by hand or hit-tested with no target, so a pad couldn't use it:
the recipe book's search box had no keyboard, a command's suggestions couldn't be
picked, a server couldn't be moved, Realms Configure's tabs had no bumpers, and
the HUD editor's footer ended its crosshair.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A book's page and signing title weren't fields, so a pad couldn't write one.
- A sign's keyboard closed on Enter after line 1, and nothing reopened it. Enter
now keeps it open, and A opens it again.
- A keyboard open over chat stayed over the death screen and took every button.
- B answered the death confirm during the second it's meant to wait.
- A guest who joined with a pad got P1's Controllers off in a new profile, so
that pad did nothing, on this join and every later one.
- Pad presses in another window could join a player.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- An idle pad's trigger rests at -1, which counted as moved past 0.5, so touching
it took over from the pad in use.
- An analog trigger jumped to full at gilrs' 0.75, so a higher threshold did
nothing.
- gilrs drops `~` from SDL rows, so sticks on the nine Linux rows with one ran
backwards, the Switch 2 Pro among them.
- The 8BitDo Ultimate 2 without a row looked up when pushed down, with X and Y
crossed.
- Rumble reached the pad from an unfocused window, adaptive triggers stayed on
over books and signs, a stale lightbar stayed after splitscreen, and in_use
outlived an unplugged pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
#10304 gave mob_to_nbt a fifth argument after the raid observation's call to it
was written, and the two merged without a conflict.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SDL rows match a firmware version exactly, and gilrs' copy has no row for
045e:0b13 at 0x0520. The pad then falls back to gilrs' default mapping: its
right stick is on ABS_Z/ABS_RZ, which nothing read, its triggers on
ABS_GAS/ABS_BRAKE, which that mapping doesn't name, and X and Y stay crossed
because it lists C and Z.
Recognise that layout from what the pad reports, so a new firmware works
without a row.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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]>
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]>
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]>
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]>
Entering a world opened the split's gilrs and closed P1's. gilrs opens a pad twice
at once, which inotify can merge into one event, and closes it twice apart. KWin
counts those events to pause its emulation, so its count could reach zero while
Horizon still held the pad, and the left stick also moved the mouse.
The router now takes P1's reader and gives it back. Its reader also gets the extra
SDL rows, which it was missing.
Refs #9730
Co-Authored-By: Claude Opus 5.5 <[email protected]>
0.5 × 255 is 127.5, which drivers can round either way: NVIDIA gives 127. The test
checks that the surface reads back exactly, so 0.75 (191) checks the same thing.
Fixes#9831
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The kernel defines BTN_X as BTN_NORTH and BTN_Y as BTN_WEST, and gilrs' default
mapping takes those names literally, so a pad with no SDL row had X and Y crossed.
Sony and Nintendo pads send the same codes by real position and stay as they are.
Closes#9813
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Graphics settings move their frame, list and key steps into settings_shell, with no change to how they behave,
and Controller Settings is its second user: General, Bindings and Device pages, search, per-row reset and Undo.
A controller notice is one slot per view rather than the split screen's own. A pad-focused options tooltip hangs
from the widget's bottom left, as the pad's hidden pointer sits mid-widget; mouse and keyboard keep vanilla's. A
focus whose widget goes away on the same screen moves beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The bumpers switch every tabbed screen, and turn the recipe book's pages; its triggers change category.
The right stick only scrolls: the focus stays on its row, no value changes, and nothing scrolls under the
on-screen keyboard. The D-pad walks a list's hidden rows, stops at a list's edge without changing a value, and
a held direction stops at the edge of the slots it's on. The crosshair stays on its target as the screen changes,
A held with the D-pad spreads a stack, and a list's scrollbar says how far its rows moved.
The arrow keys share the focus step, so they walk hidden rows too, and the superflat and single-biome rows
become focus targets for them. Focus on a preset selects it and leaves the share box to a press, as vanilla does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixes#9412. Every launcher process deleted and rewrote the one update folder, and the installer
only checked that the payload existed, so a second launcher could leave a partial file to copy over
the launcher.
- Each stage is a `<pid>-<nonce>` folder. Downloads get their name only after the hash check. Each
process removes its own; stages whose process is gone are cleaned up after an hour, counted from
the hand-over, because an installer may still be using them.
- The installer checks the archive against the signed SHA-256 again and places the launcher from
those bytes. The hash travels in an environment variable: the installer is whatever file is in
place, so the command line stays readable across versions. A window without one can't place.
- Nothing happens when the launcher in place is already that version or newer.
- On Windows a launcher running from the temp folder, however it is spelled, can't replace itself.
- Update no longer stops hosted servers. The new window doesn't stop them when it closes either.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
A machine's fake player restored its swapped fields but never ran the close
side of the use. A trader stayed in its session, a vehicle kept its menu's
stand-in, and a shulker box's opener count stayed up. It now runs the real
close paths while the fake inventory is in place, and swaps station,
craft_table, ender_open, hook, experience and the respawn point too.
Closing keeps the use as a player's: the click counts, and the same events
and sounds fire. Refusing to open would need a gate at every open site.
Fixes#9409
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
CI's launcher job and pr_ready ran only --lib, so the tests in main.rs's
modules first ran in launcher.yml's cargo test at release. Add --bins to
both. The clippy failure at forge_install_tests.rs:4 is already fixed on
main by 812512980, so it needs no change here.
Fixes#9414
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
take_loot_table dropped a held roll without clearing the table from the
chest's source compound. The emptied chest encodes the same as its decoded
source, so the save overlay wrote the source back with its table and the next
load rolled again. It now goes through consume_container_loot, as opening does.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
Each draw baked a new image for the equipped name tag, and GPUI keeps every one
in the sprite atlas until it is dropped. Marks are now kept per item id, its
file's hash and the window scale. One replaced by a scale change or a new file
is dropped from the atlas on the next frame.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
A on a focused button now presses it when let go, if the focus is still on it.
Moving the focus, a cancel, a scroll, a dialog opening or another pad taking over
drops the press. On the title, X opens Options and B focuses Quit Game; on the
pause menu, Y focuses the exit button, and A confirms.
A screen says what a button is for (`ui::action_button`, `widgets::declare_action`)
and its pad button and glyph follow: Done, Back, Cancel, Apply, Options and Exit.
Nothing else names the screen. What a screen declared stops counting once another
is drawn over it or replaces it, and `pad status` lists what is declared, what
isn't and the glyphs drawn. tools/pad_menu_e2e.py checks these with the virtual pad.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp82xRaic2UQfeBTnyhW
Keeps the Paddle checkout from main and the clock and unknown-state fix. Both sides added tests at the end of wardrobe/tests.rs and an item to the same import in wardrobe_page/shop.rs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
A fast clock closed a window the service still called open, hiding the Claim button on a set that's retired for good. Each answer's Date header now shows how far the clock is ahead, and the Shop reads the window's dates with that taken off. A clock on time or behind reads as before.
A claim state the launcher doesn't know now parses instead of failing the catalogue and the account's answer. It offers no claim and doesn't say the set is over.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
Service::item_cached built its path from catalogue values unchecked. It now refuses an entry
whose id or hash isn't 1 to 64 of a-z, A-Z, 0-9 and _, the rule the game already applies
(horizon_cosmetics::fetch), before the disk or the service is touched.
The hash isn't checked against the bytes: nothing in this repository says what it is a digest of.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp7yEJwPTJ2kw29LWdk1
Input parity (pad and keyboard reach the game the same way):
- Held keys record which device holds them; splitscreen's keyboard
handoff releases only the keyboard's, so the other player's controller
keeps its holds.
- The pad sends Shift and Ctrl through the keyboard's modifier state, so
the stack throw in inventories throws the whole stack again.
- Every bindable controller action has an explicit route, checked by a
test; screenshot and smooth camera work from a controller binding, and
pad look counts for the tutorial and zoom like the mouse.
- tools/pad_parity_e2e.py and headless Game tests compare pad and
keyboard outcomes.
Menus and inventories:
- Menus use focus; inventories, containers and the screens whose
clickable parts have no focus targets use a crosshair pointer centred on
the point it clicks. The Menu Navigation option and the right stick
click's swap are removed.
- A snapping engine settles the pointer: a glide onto the nearest slot,
flicks commit to the neighbour they head for, the D-pad hops a slot.
- Pointer speed, scroll speed and navigation repeat settings per player.
- Focus starts in vanilla's tab order, returns to the widget a screen was
left on, and only one widget is ever highlighted.
- Only a new press or real stick movement takes input from the mouse; a
screen, focus or keyboard change releases held buttons until let go.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JLHp81zX4YZ9N1mLLAwzKH
Preserve the local-server restriction for offline profiles while allowing authenticated Microsoft accounts to join external servers. Keep both translation additions and account regression tests.
Co-Authored-By: Codex GPT-6 <[email protected]>
Hand off private per-account sessions when Splitscreen is enabled, retain unavailable accounts in the picker, and connect guests with their own authentication, skins and secure-chat keys. Preserve per-launch account selection and validate the metadata UUID before accepting a session.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Codex GPT-6 <[email protected]>
Lang files: both sides' new strings kept; for keys main also added to
fr_fr.json (the Splitscreen Setup page), this branch's wording stays.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main lists monitors once a second (#6620); keep that beside this branch's
split_hud.menu in about_to_wait.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The join strip, the waiting player's prompt and the controller notices
read horizon.splitscreen.prompt.* keys (en_us, fr_fr) instead of English
literals.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Upstream's pad rule wins: in a world a pressed controller goes to waiting
players, then joins as the next player; P1 takes it only in a menu or while
waiting for one.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
heal_hold_latches releases sneak and sprint when the OS says Shift or Ctrl is up,
to recover a keyboard release that never arrived. A controller's stick click acts
as that key without setting the modifier, so the heal released it the next frame,
before a tick could start the sprint: left stick click stopped sprinting. Skip a
mapping the pad holds (or whose release waits for a tick), as modifiers::reconcile
already does.
Fixes#6377
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With prepared resources, options.on/off made it "Splitscreen: OFF", unlike
the label before localization and what the live splitscreen tests expect.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Layout icons and the Auto tooltip now come from the draft's layout index, not
from parsing the English label, so translated labels keep their glyphs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Resolve labels, status text, instructions and warnings from the language table with parameterized messages and bundled English defaults. Add French translations and a process-isolated locale/pack-override regression that preserves draft state and literal player names.
Co-Authored-By: OpenAI Codex <[email protected]>
Exercise P2’s actual controller menu path and check P1’s capture and real mouse movement before any recovery click.
Co-Authored-By: OpenAI Codex <[email protected]>
A paused client can retain its last velocity without moving; wait for actual position stability before measuring another owner’s movement.
Co-Authored-By: OpenAI Codex <[email protected]>