Commit Graph

474 Commits

Author SHA1 Message Date
Ludovic Marie bf851bfea1 Merge pull request #648 from ludovic111/feat/modpack-bench
Content mods: refuse more block states than chunks hold; the pack against NeoForge, measured
2026-10-17 05:01:42 +02:00
Ludovic Marie 1e514f8539 Merge pull request #633 from ludovic111/feat/port-pack-report
horizon-port-pack report: coverage per mod, and ATM10 measured
2026-10-17 04:58:09 +02:00
Ludovic Marie b0e31eb14f Merge pull request #632 from ludovic111/feat/neoforge-oracle-harness
Per-mod parity harness against a dev-only NeoForge oracle
2026-10-17 04:58:05 +02:00
Ludovic Marie 303c8d4452 Merge pull request #631 from ludovic111/feat/port-pack-oracle-carry
horizon-port-pack carry: templates and numbers from the NeoForge oracle, 1.21.1 loot upgraded
2026-10-17 04:58:00 +02:00
Ludovic Marie 03ca95e8af Merge pull request #630 from ludovic111/feat/mod-content-real-mods
Content mods: load real carried-over mods (large tables, convention tags, mod recipes)
2026-10-17 04:57:55 +02:00
Ludovic Marie 2822d4e1b7 Content mods: refuse more block states than chunks hold; the pack against NeoForge, measured
Loading the eight carried-over mods in the game (not only the probe) showed
two limits real packs reach. Block textures past 2048 layers are another PR;
this one handles block states and measures a pack.

- Chunk sections store a block state in 16 bits. Vanilla 26.3 has 35,723
  states; Macaw's four mods add 36,154, the eight 41,389. Past 65,536 ids
  wrapped onto other blocks (a trapdoor became a hanging sign).
  mod_content::build now refuses a set of mods past MAX_STATES and names the
  states per mod; the game then starts without content mods, as for any
  refused content.
- tools/mod_port_harness.py probes each mod on its own against vanilla, as a
  player adding it would load it.
- tools/perf_bench.py --game neoforge: Minecraft 1.21.1 on NeoForge 21.1.259
  with --mod-jars and --deps, driven by a dev-only NeoForge port of the bench
  mod (mods/horizon/src/neoforge-bench); --port-work loads a work folder's
  carried-over mods into Horizon. tools/neoforge_oracle.py installs the
  client once under .local/neoforge-oracle (the installer, then the vanilla
  libraries it inherits, sha1-checked) and builds the driver. A NeoForge
  start that crashes before the driver loads (FML reading fml.toml while
  creating it) is retried once.

Measured (docs/modpacks.md): six ported mods in Horizon against the same
mods on NeoForge, Mac mini M4 Pro, 2 games per row on a loaded machine. The
world is ready 3.1x sooner at 8 and 16 chunks, the flights run 1.7x and
3.6x the frames, and Horizon's RSS is not lower (+1.1 GB with the mods).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:55:20 +02:00
Cosmic 8fa5c38663 Add an optional Fullbright extra 2026-10-16 21:42:01 -04:00
Ludovic Marie 0560bbf876 horizon-port-pack report: coverage per mod, and ATM10 measured
`report <work>` writes coverage.json and coverage.md: per mod ported,
partial, not yet or not needed with the reason, and the totals by class,
tier and licence family. It reads the inventory, each mod's carry report
and its harness result: ported is a green harness with nothing left out
(blocks with a class of their own are counted and named); partial lists
the failing checks, the blocks left out and what the templates miss; not
yet names the API tier or the dependency it waits for; not needed covers
libraries, performance mods and pack tooling. Unclassified mods are not
yet, never not needed.

`import` also reads Crash Assistant's modlist.json, the list ATM10
publishes in its repository (mod ids, names, versions, Modrinth hashes,
the loader entry), and the inventory uses its mod ids and names when
there is no jar.

ATM10 (496 files, the eight mods we downloaded carried and checked):
ported 1, partial 7, not yet 367, not needed 121.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:28:43 +02:00
Ludovic Marie 32f5f01e22 Per-mod parity harness against a dev-only NeoForge oracle
The harness the modpack design calls for, for data-tier mods:

- tools/neoforge_oracle.py installs a NeoForge 21.1.259 server (ATM10's)
  under .local/neoforge-oracle, builds the exporter in
  mods/horizon/src/neoforge-oracle (our code, never shipped), runs it
  headless with the user's jars and writes every block (class chain,
  states, per-state hardness, light, tool need, map colour, sounds,
  outline and collision; drops for hand, diamond tools, shears and silk
  touch; placement; BlockSetType and WoodType), item, recipe and tag of
  the mods' namespaces, plus the vanilla blocks for templates. About 30 s.
- examples/mod_port_probe.rs builds the content root from the carried
  mods and dumps what Horizon's loaders make of the same things.
- tools/mod_port_harness.py compares them per mod and writes
  <work>/mods/<id>/harness/result.json. 1.21.1 sound events map to 26.3's
  through vanilla blocks both have, and vanilla items newer than 1.21.1
  are not counted against a recipe.

On ATM10's own jars (MIT): Macaw's Trapdoors passes everything; Doors,
Fences, Paths, Glassential, Factory Blocks and Cloud Glass pass every
drop, recipe, tag and placement and fail only on what API 0.2 cannot say
(shapes and sound events of their own, blocks of classes no vanilla block
shares). Jars, facts and results stay in the work folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:27:20 +02:00
Ludovic Marie 886def4360 horizon-port-pack carry: templates and numbers from the NeoForge oracle, 1.21.1 loot upgraded
Carrying over seven real ATM10 mods (Cloud Glass, Factory Blocks,
Glassential, Macaw's Doors, Fences, Paths and Trapdoors) showed what the
guesses missed:

- Loot tables: 1.21.1's `conditions` lists, `functions` and
  `condition`/`function` kinds are now 26.3's single `condition`,
  `modifier` and `type`; silk-touch and shears tool matches become
  `minecraft:tool/can_silk_touch` and `can_shear`. Before, every drop
  condition was ignored (glass dropped itself by hand, upper door halves
  dropped doors, double slabs dropped one).
- With the oracle's facts (<work>/oracle/facts.json, from
  tools/neoforge_oracle.py in the next PR), every registered block gets
  the vanilla template of the same vanilla class and states that matches
  its shapes, sounds, BlockSetType and WoodType best, and the mod's own
  hardness, blast resistance, light, tool need, friction, map colour and
  sounds. Items get stack size and rarity. The carry report lists what a
  template does not reproduce and why a block was left out.
- Convention tags a recipe uses (c:dyes/red) are written from the
  oracle's tags, vanilla and own members only; the jar's own c tags stay
  inside the mod.
- Without the oracle, blockstates that omit properties their models
  ignore (powered, waterlogged) still find their template, and names pick
  between fences and panes, doors and trapdoors.
- `version = "${file.jarVersion}"` is read from MANIFEST.MF.
- `--resources <prepared>` limits templates to blocks Horizon has.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:25:54 +02:00
Ludovic Marie ddc115e969 Content mods: load real carried-over mods (large tables, convention tags, mod recipes)
Loading seven real NeoForge mods carried over by horizon-port-pack (about
1,000 blocks) broke the content root in four places:

- The per-state NBT tables (cached collision, occlusion, movement, worldgen
  block properties and support) passed the NBT reader's 1M-node cap once
  mod states were appended. They are trusted prepared tables with a row
  per state, so they now read and write with Limits::tables() (16M nodes,
  256 MiB) everywhere they are loaded.
- The overlay linked the base root by the path it was given, so a relative
  base gave dangling links. The base is made absolute first.
- Carried-over tags name blocks the port left out and other mods' items.
  Vanilla fails such a tag as a whole; entries naming ids nobody registers
  (or tags nobody defines) are now dropped, so the rest loads.
- Mods use the convention tags (c:dyes/red, c:glass_blocks/colorless).
  Content mods may add to data/c/tags/ as to vanilla's, and with content
  mods loaded the tag loaders read the c namespace and the mods' own.

Stonecutter and smithing recipes in mod namespaces load too (ids
<ns>:<name>), as crafting and cooking ones already did. With no content
mod nothing changes: the loaders read the minecraft namespace only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:24:34 +02:00
Ludovic Marie 9c03535011 docs/server.md: hosting a Horizon server, the design
What the dedicated server does today (measured against vanilla 26.3), the horizon-server supervisor and
its packaging, server.toml, the CLI and TUI, the web panel, backups and updates, the control API and MCP,
the bot benchmark plan against vanilla and Paper, the working version for 2026-10-23, and the PRs that
build it (#551 to #556).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 22:22:56 +02:00
Ludovic Marie 673ac84724 horizon-port-pack carry: a jar's data and assets as a content mod
carry <work> [<mod id>...] turns each content mod with a jar into a folder
content mod (API 0.2) in <work>/mods/<id>/carry/, for local use:

- assets/<id>/, data/<id>/ and data/minecraft/tags/ are copied; other
  namespaces and vanilla data go to outside/ for review; no class file is
  copied.
- Vanilla-type recipes get 26.3 ingredients ("id", "#tag", lists);
  custom types and NeoForge conditions are kept and listed.
- Item models get the items/ client definitions 1.21.4 introduced.
- Each block is declared from a vanilla template guessed from its
  blockstate properties; unknown property sets are listed. Items
  without a block are declared from minecraft:stick.

The test builds a small NeoForge jar, carries it over, and reads the
result with horizon-mods' own Content reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:25:28 +02:00
Ludovic Marie 85858610bf horizon-port-pack: import a modpack and classify its mods
A CLI for the first steps of docs/modpacks.md's pipeline:

- import: a Modrinth .mrpack, a CurseForge zip or a Prism / MultiMC
  instance (or a folder of jars) to pack.json: Minecraft and loader
  versions, each file with its hash and sides, and the overrides.
- inventory: Modrinth metadata by SHA-1 (cached in modrinth.json, so
  reruns work offline), and the jars when the user's instance has them
  (neoforge.mods.toml / mods.toml / fabric.mod.json and a file census,
  without decompiling). Each mod gets a class and API tier from the
  reviewed ATM10 table, then rules on categories, sides and the census,
  and a dependency layer. Writes inventory.json and inventory.csv.

Only metadata is downloaded; jars are never fetched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:25:04 +02:00
Ludovic Marie 746c888cd3 horizon-mods: content mods (API 0.2), blocks and items declared from vanilla templates
A .hmod may carry assets/ and data/ beside mod.wasm, or instead of it. The
manifest needs api = "0.2" and the new registries capability ("add blocks
and items to your worlds"). Content mods have no WIT interface: code still
links API 0.1.

horizon_mods::content::Content reads a mod's content and checks it:
- plain resource paths only;
- 65,536 files, 16 MiB each, 512 MiB in all;
- files in the mod's own namespace, plus additions to vanilla tags;
- blocks and items declared in data/<id>/horizon/{block,item}/*.json, each
  copying a vanilla template with typed overrides (hardness, resistance,
  light, tool, friction, map colour, sounds; stack size, rarity), unknown
  keys refused.

Mods::load refuses an enabled mod whose content fails these checks, with
the reason on its row. A content-only mod is on with no engine and no hooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:23:35 +02:00
Ludovic Marie a06367edf1 docs: modpacks design, with ATM10's real mod list classified
docs/modpacks.md: how any pack (ATM10 down to small Fabric packs) gets to
Horizon: import (.mrpack, CurseForge zip or instance, Prism, packwiz),
inventory and classification, dependency order, mechanical carry-over of
data and assets, one agent per mod for code with a per-mod harness against
a dev-only NeoForge/Fabric oracle, and a coverage report. Also the mod API
gaps against 0.1, the registries 0.2 design (template blocks through the
overlay root), licence handling, how "10x" is measured against NeoForge,
and the scope for 2026-10-23.

docs/modpacks/atm10-mods.csv: ATM10's 495 jars from the pack's own
crash_assistant modlist (2026-10-15), with Modrinth metadata looked up by
hash: class, API tier, licence, sides. Metadata only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:08:23 +02:00
Ludovic Marie 81243f7897 Release notes: drop the sprint-jumping known issue
Nobody could reproduce it, and no issue tracks it, yet every release since
2026-10-14 repeated it. Players noticed (rdbt on Discord).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:29:46 +02:00
Ludovic Marie fc59063ada Keep held keys held through wait on the control socket
`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>
2026-10-16 10:56:48 +02:00
Ludovic Marie 3cfb7b8053 Mods' commands in the chat's command tree (#456)
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>
2026-10-16 06:48:56 +02:00
Ludovic Marie 9b9b23bf3a Merge pull request #436 from ludovic111/feat/shaderpack-client
horizon-client: run a Java pack's full-screen passes over the world
2026-10-16 05:31:32 +02:00
Ludovic Marie 953cdc947f Merge pull request #418 from ludovic111/feat/shaderpack-passes
horizon-shaderpack: plan a Java pack's full-screen passes like Iris
2026-10-16 05:31:28 +02:00
Ludovic Marie 4de37ac0e4 Merge pull request #415 from ludovic111/feat/shaderpack-properties
horizon-shaderpack: read shaders.properties like Iris
2026-10-16 05:31:23 +02:00
Ludovic Marie 1bf51d786f Merge pull request #413 from ludovic111/feat/shaderpack-translate
Java shader packs (2): translate programs to WGSL (syntax-tree legaliser + naga)
2026-10-16 05:31:19 +02:00
Ludovic Marie 64a82acdab Merge pull request #405 from ludovic111/feat/shaderpack-preprocess
Java shader packs (1): horizon-shaderpack crate, Iris's preprocessor and options
2026-10-16 05:31:14 +02:00
Ludovic Marie 2862f17e86 horizon-client: run a Java pack's full-screen passes over the world
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>
2026-10-16 00:22:39 +02:00
Ludovic Marie 5350f3465b Merge pull request #424 from ludovic111/test/lan-e2e
tools/lan_e2e.py: a LAN host and guest, checked from both sides
2026-10-16 00:10:22 +02:00
Ludovic Marie 08c4805e10 Release train: pad the same-day counter to two digits
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>
2026-10-15 23:17:09 +02:00
Ludovic Marie e1d9ae1dd7 horizon-shaderpack: plan a Java pack's full-screen passes like Iris
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>
2026-10-15 22:36:18 +02:00
Ludovic Marie 38a9f6543f horizon-shaderpack: read shaders.properties like Iris
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>
2026-10-15 22:36:18 +02:00
Ludovic Marie 98ebdcad7b horizon-shaderpack: translate Java shader pack programs to WGSL
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>
2026-10-15 22:36:18 +02:00
Ludovic Marie b22583d057 horizon-shaderpack: open Java shader packs and preprocess them like Iris
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>
2026-10-15 22:36:18 +02:00
Ludovic Marie d28521f765 Merge pull request #362 from ludovic111/feat/dualsense-lights
DualSense lightbar and player LEDs
2026-10-15 22:31:10 +02:00
Ludovic Marie 7d6936e000 Merge pull request #361 from ludovic111/feat/dualsense-vibration
DualSense vibration: shaped pulses, HD haptics on USB (opt-in)
2026-10-15 22:30:54 +02:00
Ludovic Marie 5419ef0ed4 Merge pull request #360 from ludovic111/feat/dualsense-triggers
DualSense adaptive triggers
2026-10-15 22:30:46 +02:00
Ludovic Marie b66eea6506 Merge pull request #348 from ludovic111/feat/dualsense-hid
DualSense output over HID: the report, a writer thread, and rumble through it
2026-10-15 21:52:08 +02:00
Ludovic Marie fd1acd5ed7 tools/lan_e2e.py: a host and a guest on LAN, checked from both sides
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>
2026-10-15 20:56:45 +02:00
Ludovic Marie 3761f2a5fb Sort buttons for the inventory and chests, an extra off by default
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>
2026-10-15 19:44:18 +02:00
Ludovic Marie 05a1e10bbe Mouse Tweaks-style inventory controls, an extra off by default
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>
2026-10-15 19:44:18 +02:00
Ludovic Marie f8bcfcfec6 DualSense lights: pin the host defaults, document quit and the mute LED
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>
2026-10-15 19:23:05 +02:00
Ludovic Marie be57f82a07 DualSense HD haptics: Windows' device name, the arming rule, Bluetooth notes
- 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>
2026-10-15 19:23:05 +02:00
Ludovic Marie 6663af199e DualSense triggers: catches at the press point, shields, official modes only
- 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>
2026-10-15 19:23:05 +02:00
Ludovic Marie 6086758f94 DualSense output: rumble, LEDs and Bluetooth checked against SDL and the kernel
- 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>
2026-10-15 19:23:05 +02:00
Ludovic Marie df3244d335 DualSense lightbar and player LEDs
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>
2026-10-15 19:23:05 +02:00
Ludovic Marie 0681064767 DualSense vibration: shaped pulses, HD haptics on USB (opt-in)
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>
2026-10-15 19:23:05 +02:00
Ludovic Marie a316768bfc DualSense adaptive triggers
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>
2026-10-15 19:23:05 +02:00
Ludovic Marie ac955934b5 DualSense output over HID: the report, a writer thread, and rumble through it
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>
2026-10-15 18:38:10 +02:00
Ludovic Marie b8ed3a113a Merge pull request #326 from ludovic111/docs/mods-design
Mods design: Horizon's Rust mod system and what carries over from Java mods
2026-10-15 18:02:26 +02:00
Ludovic Marie 68cbad54a3 Merge pull request #351 from ludovic111/feat/agent-waits
Agent control: wait for a state, and the log of what happened
2026-10-15 17:43:54 +02:00
Ludovic Marie 24a702181a Merge pull request #350 from ludovic111/feat/agent-options
Agent control: read and set every option by key
2026-10-15 17:35:49 +02:00
Ludovic Marie e6588a13f3 Merge pull request #349 from ludovic111/feat/agent-ui-tree
Agent control: the widget tree of every screen, click and set by id
2026-10-15 17:33:58 +02:00