`wait` ended with release_agent_keys like every other task, so
`key w down` + `wait 40` + `wait 20` walked for the first wait only.
A wait holds nothing: it leaves keys and buttons alone. They stay held
until `key ... up`, `release` or `stop`. Documented in horizon-ctl's
key/wait help (and so the MCP tools) and docs/agent-control.md.
Fixes#469
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A mod's commands ran but the chat box parsed against vanilla's tree only,
so `/coords` showed red as unknown and never completed. The game now keeps
the chat's tree as vanilla's plus a literal per running mod's command (every
permission level, optional greedy `args`), rebuilt when mods start, reload
or stop.
Fixes#456
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A folder holding shaders/, or a .zip, in shaderpacks/ now loads from the
Shader Packs screen: every planned begin, prepare, deferred, composite
and final pass is translated and drawn each frame over Horizon's world,
with Iris's formats, clears and buffer flips.
The targets keep OpenGL's layout (the translator flips gl_Position and
remaps its depth), the world enters as OpenGL depth, and the pack gets
OpenGL matrices. Textures share four samplers by kind (Metal allows 16
per stage), and isnan/isinf test bits (Metal's fast math folds x != x).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub's releases page sorts a day's tags as text, so v2026.10.15.9 showed
above v2026.10.15.18 (reported by rdbt on Discord). Tags now count .01, .02...
Every reader (release_train.tag_key, the launcher's compare_versions) compares
the parts as integers, so older unpadded tags still order correctly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The begin, prepare, deferred, composite and final passes of a program
folder: what each writes (the later of DRAWBUFFERS and RENDERTARGETS),
which side of every ping-ponged colour target it reads (Iris's
BufferFlipper, with flip.* overrides), mipmapped targets, scale.*, and
the targets' formats, clearing and sizes from the consts of every pass
in Iris's order. The preprocessor now keeps the directive comments of
active code, as Iris's does, so a DRAWBUFFERS in an #if'd out branch
doesn't count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Java .properties parsing, the two reads Iris makes (the raw file for
screens, sliders and profiles; the preprocessed one, with enabled options
and the environment's macros defined, for everything else), option
screens, profiles and the current one, program.*.enabled through a port
of Iris's BooleanParser, image formats for shadowcomp's writeonly
images, and blend overrides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The legaliser of docs/shader-packs.md step 1, on a syntax tree (glsl-lang)
instead of the spike's tokens: attribute/varying to located in/out with
matrix and array varyings split, loose uniforms in one std140 block that
extends across a pass, combined samplers split into texture + sampler
bindings (through function parameters, respecting locals that hide them),
compatibility built-ins and Iris's core-profile names declared, legacy
texture and shadow calls, hoisted global initialisers, sized unsized
arrays, isnan/isinf/modf helpers, and dead code after a jump dropped (the
spike's 'naga constructor bug' in BSL was code after discard). naga then
parses, validates and writes WGSL. The programs of one pass share a Link
so locations, bindings and the uniform layout agree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new crate for the OptiFine/Iris translation (docs/shader-packs.md, order
step 1, first half): open a pack folder or zip (shaders/ found as Iris finds
it), list its programs per dimension folder, build the #include graph,
discover options per include component (OptionAnnotatedSource rules: boolean
defines referenced by #ifdef, value defines and configurable consts with a
[...] list, conflicting defaults dropped), write the player's values into
the lines, and expand Iris's standard macros with pp-rs, keeping line
numbers so errors name file:line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two Horizon games under horizon-ctl, each with its own scratch home: the
host opens a survival world to LAN with commands, the guest joins by
address, and each check (tab, self, armour, nametags, chest, crafting,
damage) is reported on its own with screenshots of both sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Extras page's Inventory section gains "Sort buttons" (sort, off) and
"Sort key" (sortKey, unbound). A small button right of a chest, barrel,
ender chest or shulker box title sorts the container; one right of the
"Inventory" line sorts the main inventory (the hotbar stays). The key
sorts the part under the cursor.
inv_sort::plan merges partial stacks, then orders the stacks by creative
tab order (the search tab's), then name, full stacks first, using only
left PICKUP clicks with an empty hand: the game sends them as ordinary
clicks, so vanilla servers accept them. A unit test replays the plans
through the menu engine on 40 random chests.
The Extras page binds any extra key the way it bound the zoom key
(Item::key_name, Extras::set_key).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Extras page gains an Inventory section with "Mouse Tweaks controls"
(mouseTweaks, off) and "Scroll wheel moves items" (mouseTweaksWheel). In
the player inventory and chest-like containers, locally and on servers:
- right-drag holding a stack puts one item in each slot entered, at once;
- left-drag holding a stack gathers the slots of the same item into the
hand, while vanilla's drag covers one slot (a drag over empty slots
still splits as vanilla does); with Shift they are quick-moved;
- Shift + left-drag with an empty hand quick-moves every slot entered;
- the wheel over a slot moves one item to the other side (down) or
brings one back (up).
Every action is ordinary slot clicks: the menu engine's, or ContainerClick
packets for any slot (remote_slot_click, factored out of remote_click).
With the extra off each hook returns at once.
horizon-ctl gains drag-at and scroll-at, which send the window events of
a mouse drag and of wheel steps, to test these and vanilla's drags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests pin player 1 (0x04), the default blue (0, 0, 128) and the instant
bit (0x20) to hid-playstation.c and SDL; the docs say what Horizon resets
on quit (more than SDL or the kernel do) and that the mute LED is never
touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The pad's audio device is also found by Windows' "Wireless Controller"
endpoint name (the four channels rule out a DualShock 4); a name test.
- docs/controllers.md: the haptics need reports without the rumble flags
(SDL, nondebug/dualsense, dse-haptics), which Horizon now always sends while
they are open; Windows needs the Quadraphonic speaker setup; on Linux ALSA
can't open the card while PipeWire holds it; Bluetooth haptics (report
0x32, SAxense) are documented and not done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A loaded crossbow and the boosts (rocket, pig, strider) get a weapon
catch that breaks in the zone where the trigger threshold presses, so the
click is the shot (the boost was a feedback wall that never gave way).
- Draws start at zone 2 and stop at 6 of 8 (held for as long as you like).
- A shield in the use hand weighs (feedback 2, 3); disabled by an axe, the
trigger locks (feedback 1, 8), as Deathloop does for a jammed gun.
- Mining buzzes slower on harder blocks (40/30/20/10 Hz).
- A 0 Hz vibration frees the trigger (Nielk1's factory); tests pin the
bytes to DSX++'s and dualsensectl's examples.
- docs/controllers.md: the table with the effect of each action and why.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every report carries the motors; still motors send no rumble flags, as SDL
stops rumble (a report without them ends the emulation and re-arms the
audio haptics), so a trigger change no longer cuts a vibration.
- Legacy vibration at half strength (SDL); the Edge always takes V2; an
unreadable firmware version falls back to legacy.
- The first report after opening clears the motor/trigger attenuation
(flags 1 bit 6, byte 36; dualsensectl, pydualsense).
- The LED reset goes in a report of its own before any colour
(hid-playstation's dualsense_reset_leds); over Bluetooth the colour waits
for the connection animation (SDL's sensor clock check).
- Bluetooth output only where gilrs keeps reading the pad (Linux), else with
HORIZON_DUALSENSE_BT=1: any output switches the pad to its full report.
- USB is preferred when the pad is on both.
- Tests pin the reset report and its CRC (zlib), the flags and the clock offset.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lightbar shows the player's health (green to amber to red) or the
dimension, at half brightness, chosen in the controller options (Lightbar:
Health / Dimension / Off; health by default). The player LEDs show player 1.
When Horizon lets go of the pad, the lights it changed go back to a host's
defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each rumble event plays a short shape instead of one flat block: a hit
starts at full strength and dies away, an explosion rolls on, a totem
swells, a broken block is a tick. They drive the motors' emulation in the
HID report.
On USB the pad is also a four-channel audio device whose channels 3 and 4
drive its actuators: with HORIZON_DUALSENSE_HAPTICS=1 the shapes play there
as waveforms (80 Hz and 200 Hz tones for the heavy and light parts),
channels 1 and 2 silent, until that path is confirmed on a real pad.
docs/controllers.md says what each OS and transport allows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each trigger feels the action bound to it: resistance growing while a bow
draws, a trident charges or a crossbow loads, a gun trigger's click on a
loaded crossbow, a wall where pulling boosts (a rocket while gliding, the
stick of a ridden pig or strider), and a light buzz while mining, scaled by
the block's hardness. Free in menus, paused, unfocused, on another pad,
with controllers off, and on quit. On by default, "Adaptive Triggers" in
the controller options turns them off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The horizon-dualsense crate builds the DualSense's output report (USB 0x02,
Bluetooth 0x31 with its CRC-32 seeded with 0xA2) and writes it with hidapi
from a thread. The game sends its rumble pulses through it when the active
pad is a DualSense, so vibration now works on macOS too, and falls back to
gilrs force feedback when the pad can't be opened. Quitting frees the
triggers and stops the motors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design sections now describe the implementation: ids for rows,
switches and segments, the redraw wait, wait-for --since and event KIND,
the events log (subscribe comes next), the launcher proxying through
horizon-ctl mcp. A before/after table and the PRs of each step replace
the plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wait-for screen NAME|widget ID|loaded|chat TEXT|event KIND replies once
the game gets there (--timeout, default 30 s, ends in an error of kind
timeout; --since SEQ counts chat and events from an earlier point), so
scripts and agents stop polling. events [--since SEQ] lists what
happened, oldest first, numbered: screens, chat lines, deaths, respawns,
items gained or lost, dimensions, worlds opened and closed. The state is
compared once per event loop turn and only under --control, so watching
never changes the game. respawn now says when the death screen's button
is not active yet instead of clicking it to no effect.
horizon-ctl and its MCP gain both; tools/agent_control_e2e.py gains an
events scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
options-get [KEY...] gives every options.txt option by its vanilla key
with its value and what it accepts (type, range, step or choices; enums
by name, key bindings as key.keyboard.w). options-set KEY VALUE sets one,
applies it at once and saves options.txt as the options screen does
(language and resource packs reload too); a value the option refuses is
an error naming what it accepts, and the option keeps its value. After a
set, ui reads the redrawn screen.
horizon-ctl and its MCP gain both; tools/agent_control_e2e.py gains an
options scenario.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Discord server banner (page + PNG), the server icon render and Kira's avatar (SVG master +
render) now live in the private kira repo (ludovic111/kira#1). xtask icons no longer renders them;
the brand README points there, and CONTRIBUTING states what belongs in this repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>