mirror of
https://github.com/horizon-rust/horizon-mods.git
synced 2026-11-07 11:05:58 -10:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e62054006e | ||
|
|
b956bda63b | ||
|
|
ce12d21fe3 | ||
|
|
34f8420003 | ||
|
|
ed256db6f7 | ||
|
|
4fb24a8263 | ||
|
|
2cc76d1dc5 | ||
|
|
ef5c5fe50b | ||
|
|
c1c783574c | ||
|
|
4b3311b800 | ||
|
|
b022d5b14f | ||
|
|
3a8c438ec5 | ||
|
|
adea72cbdf | ||
|
|
bc79048b9a | ||
|
|
fb9ab126f7 | ||
|
|
4d975b9813 | ||
|
|
e7a95dcbef | ||
|
|
266b6039d4 | ||
|
|
22c6f14ea6 | ||
|
|
0f556b3a89 | ||
|
|
d6a1ddd703 | ||
|
|
f539b5a584 | ||
|
|
b07b1fdef0 | ||
|
|
b6a69cc40e |
No files matched your search
@@ -65,7 +65,7 @@ why.
|
||||
| `.github/workflows/publish.yml` | On merge: releases, then `index.json` on Pages |
|
||||
| `.github/workflows/cleanup.yml` | Weekly: staging files nobody needs any more |
|
||||
| `.github/workflows/native.yml` | Horizon's native mods: each changed one's tests, `.hnmod` and game check, all of them together, and weekly on the newest Nightly |
|
||||
| `native/<name>/` | Horizon's own native mods (Zoom, FPS Counter, Keystrokes, Minimap): the source of their `.hnmod` files |
|
||||
| `native/<name>/` | Horizon's own native mods (Zoom, FPS Counter, Keystrokes, Minimap, Recipes): the source of their `.hnmod` files |
|
||||
| `native/tools/` | Shared by them: apply on Horizon, check, build, package, prove (`native/tools/native.py`) |
|
||||
| `scripts/` | What the workflows share |
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
id = "biomecompass"
|
||||
kind = "native-mod"
|
||||
name = "BiomeCompass"
|
||||
summary = "Craft a Biome Compass, pick a biome and its needle points to the nearest one."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
Craft a **Biome Compass** (a compass with a sapling on each of its four sides, any kinds), right-click it and pick a biome from the list (with a search box). The world generator is searched around you, a little every tick, and the compass's needle then points to the nearest one, with its distance and direction on the action bar.
|
||||
|
||||
- Works in single player, on LAN and on a server running the native build, for vanilla clients too (the picker is a vanilla dialog).
|
||||
- `/biomecompass find|list|status|stop|tp` as well; a small HUD box is optional.
|
||||
- Settings: search radius, sample spacing, per-tick budget, HUD.
|
||||
|
||||
Inspired by [Nature's Compass](https://modrinth.com/mod/natures-compass); not affiliated, no code from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/biomecompass"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/biomecompass/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.2.2"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "biomecompass-0.2.2.hnmod"
|
||||
sha256 = "d9a4aedcf22318774e1ac6bed6f1e00aca13a38c8dc3426d5a7800e4d8c61bf5"
|
||||
changelog = "First release."
|
||||
+10
-10
@@ -1,22 +1,22 @@
|
||||
id = "blockscope"
|
||||
kind = "mod"
|
||||
name = "Blockscope"
|
||||
summary = "The block under your crosshair: its name, id and distance, in a box beside the crosshair."
|
||||
description = """
|
||||
Jade-style block info for Horizon: the block under your crosshair, its name in your language, its block id and how far it is, in a small box beside the crosshair.
|
||||
|
||||
- Nothing is drawn when you look at no block, while a screen is open, or with the HUD hidden (F1).
|
||||
- Reaches 20 blocks and flips to the left near the screen's right edge.
|
||||
|
||||
It reads the player and one crosshair raycast, draws on the HUD, and reads the item table for block names. No chat, no storage, no commands.
|
||||
|
||||
Hardness, the right tool, block states and entities are out of reach in mod API 0.1: they need the future `world-read` and crosshair `target` interfaces."""
|
||||
summary = "The block under your crosshair: its name, id, distance and icon, with reach, placement and opacity on its own settings page. A Jade-style tooltip for Horizon."
|
||||
description = "Jade-style block info for Horizon: the block under your crosshair, its name in your language, its block id, how far it is and its icon."
|
||||
authors = ["Domanffe"]
|
||||
owners = ["Domanffe"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Domanffe/blockscope"
|
||||
icon = "https://raw.githubusercontent.com/Domanffe/blockscope/main/docs/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.2.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "blockscope-0.2.0.hmod"
|
||||
sha256 = "dcb8a3d3eefe37ebc7fd1886412dc9799c5fa4dee88fd419e7f46a712df00c12"
|
||||
changelog = "Reach, placement, background, coordinates and the item icon on its own settings page (Options > Mods > Blockscope), kept between games."
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
published = "2026-10-27"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
id = "carryblock"
|
||||
kind = "native-mod"
|
||||
name = "Carry Block"
|
||||
summary = "Sneak and right-click with an empty hand to pick up a chest (with its items) or a small mob, carry it and put it down."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
Sneak and right-click with an empty hand to pick up a block with its contents (chests, furnaces, barrels...) or a small, friendly mob, and carry it; right-click to put it down where you want, with everything it held.
|
||||
|
||||
- Slowness while carrying; a small HUD box shows the load.
|
||||
- Never an item: nothing to duplicate. On death the load is put down nearby, or dropped as its item with its contents.
|
||||
- Follows the game's rules: spawn protection, world border, reach, adventure mode, game master blocks; no pick-up of a container someone has open, a mob being traded with, a monster, a boss or another player's pet. Spawners and similar are denied by default.
|
||||
- Deny lists, survival/creative rules and slowness are settings (Options → Mods → Carry Block).
|
||||
|
||||
Inspired by [Carry On](https://modrinth.com/mod/carry-on); not affiliated, no code from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/carryblock"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/carryblock/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.2"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "carryblock-0.1.2.hnmod"
|
||||
sha256 = "9a6ebd26f640a477815862eb36846fe3289046d0f4c06ef3a815f463ca9a3701"
|
||||
changelog = "First release."
|
||||
@@ -0,0 +1,62 @@
|
||||
id = "classic_zoom"
|
||||
kind = "mod"
|
||||
name = "Classic Zoom"
|
||||
summary = "Hold V to zoom 3x, scroll for more, release to snap back. Instant, no settings, inspired by WI Zoom."
|
||||
description = """
|
||||
## Controls
|
||||
|
||||
| Action | Result |
|
||||
|---|---|
|
||||
| Hold **V** | Zoom in 3x |
|
||||
| Scroll while holding V | Zoom from 1x to 10x, x1.1 per wheel step |
|
||||
| Release V | Back to normal view at once |
|
||||
|
||||
While V is held, the mouse wheel zooms instead of switching hotbar slots, and turning slows down in step with the zoom so aiming stays steady. Rebind the key on the mod's page in Options > Mods.
|
||||
|
||||
## How it differs from Horizon's own Zoom
|
||||
|
||||
| | Horizon Zoom | Classic Zoom |
|
||||
|---|---|---|
|
||||
| Default key | Z | V |
|
||||
| Starting zoom | 4x (configurable) | 3x |
|
||||
| Range | 1.25x to 10x | 1x to 10x |
|
||||
| Wheel step | x1.25 | x1.1, finer |
|
||||
| Transition | Smooth by default | Instant |
|
||||
| Settings page | Yes | None |
|
||||
|
||||
Both can be installed at once: they use different keys.
|
||||
|
||||
## Notes
|
||||
|
||||
- Horizon caps zoom at 10x, so there is no 50x as in WI Zoom.
|
||||
- Written from scratch for Horizon's mod API. Not affiliated with Wurst-Imperium.
|
||||
- Some servers forbid zoom mods: check the rules before you join.
|
||||
|
||||
Source and issues: [github.com/SU4G3/classic-zoom](https://github.com/SU4G3/classic-zoom)"""
|
||||
authors = ["Surge"]
|
||||
owners = ["SU4G3"]
|
||||
license = "GPL-3.0-or-later"
|
||||
source = "https://github.com/SU4G3/classic-zoom"
|
||||
icon = "https://github.com/SU4G3/classic-zoom/blob/main/.github/icon.png?raw=true"
|
||||
key = "RWTOxMcIG6MtC0QQnhWEMC+nLKJ6+u4trV/rF+4juSRuow8W4b5P+CPS"
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "classic_zoom-1.0.0.hmod"
|
||||
sha256 = "d8bc299202d9760299a3901841adef950d46cfcc1a54e0729379d24885e9ca5d"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key
|
||||
RUTOxMcIG6MtC63o8q5cdLDamPDTVjscmZ1LMdBd8xhKmsCNy6c77sBps3h65Z2/AO+apNyH6zkbnViYtwI3eAQjgrhJvR88cQs=
|
||||
trusted comment: timestamp:1793265658\tfile:classic_zoom-1.0.0.hmod\thashed
|
||||
QLysIbvTRiUzChl9NiUchhmO1Nn4nh4HLkJwtseTccYm3YsKz4nVieLsC6tIY6xItPGDT/43EWu89GhlnO60DA=="""
|
||||
changelog = """
|
||||
Initial release.
|
||||
|
||||
- Hold V to zoom in 3x; release to snap back to normal view.
|
||||
- Scroll while holding V to zoom from 1x to 10x (x1.1 per wheel step); the hotbar doesn't move while zooming.
|
||||
- Instant zoom with no transition, in the style of WI Zoom.
|
||||
- Mouse turning slows down in step with the zoom for steady aiming.
|
||||
- Key rebindable on the mod's page in Options > Mods.
|
||||
- Permissions: render (camera zoom) and input (the zoom key and mouse wheel) only."""
|
||||
@@ -0,0 +1,39 @@
|
||||
id = "compass_bar"
|
||||
kind = "mod"
|
||||
name = "Compass Enhanced"
|
||||
summary = "A compass bar with degrees and colored directions, plus death points that guide you back."
|
||||
description = """
|
||||
A compass bar at the top of your screen that shows where you are facing, in degrees, and slides smoothly as you turn.
|
||||
|
||||
## Features
|
||||
- Compass letters (N, NE, E, SE, S, SW, W, NW) in a color you choose
|
||||
- Death points: every time you die, a marker stays on the bar with the distance to where you died
|
||||
- Reaching a death point turns it into an X with a countdown, or look at it and press X to dismiss it
|
||||
- Arrows with the distance at the edge of the bar for death points out of view
|
||||
- Optional background box and four sizes (75% to 150%)
|
||||
- No commands
|
||||
|
||||
Settings are in Options → Mods → Compass Bar. Death points are not saved: they vanish when you close the game."""
|
||||
authors = ["Isaac"]
|
||||
owners = ["Fernandes585"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Fernandes585/Compass-Enhanced"
|
||||
key = "RWScU5xtTnTnu55tXSTPbhM2BQ6KG2FLeMD9aTeX111NlQPh7cCafm1w"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
published = "2026-10-28"
|
||||
game = "26.3"
|
||||
file = "compass_bar-0.1.0.hmod"
|
||||
sha256 = "bb8f191f89f5ea963301fd2d28315b251dfb8df9656b640975eddffaec3e6b00"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key\r
|
||||
RUScU5xtTnTnuz0T7EECJHdaPC5PHAOud+04Uwen4mw75K38TKOCnK9eCKn44pX113qFMvkrP+P4CwF9FJBFX5qws5BedBPbpA0=\r
|
||||
trusted comment: timestamp:1793221575\tfile:target/horizon-mod/compass_bar.hmod\thashed\r
|
||||
DedLxzyaxaXaNO8y8pICM4Th3nL0N4oxamDGm0eoQb+6UDBj6pLXbtQLxqnMrbDQYmCE2gX6PGS86G1C33GGDg=="""
|
||||
changelog = """
|
||||
First release.
|
||||
|
||||
- Compass bar with degrees and compass letters (N, NE, E, SE, S, SW, W, NW)
|
||||
- Death points with distance, arrival countdown and a key to dismiss them (X by default)
|
||||
- Settings: bar on/off, background, scale, letter color, arrival distance, X time and death points kept"""
|
||||
@@ -0,0 +1,26 @@
|
||||
id = "enchantmenthints"
|
||||
kind = "native-mod"
|
||||
name = "Enchantment Hints"
|
||||
summary = "A short description under every enchantment in item tooltips, in English and Russian."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
Every enchantment on books and gear gets a one-line description in its tooltip: what it does, in plain words. Our own text for every 26.3 enchantment, in English and Russian.
|
||||
|
||||
- Always, or only while Shift is held (with a reminder line).
|
||||
- Colour and line width are settings.
|
||||
|
||||
Settings: Options → Mods → Enchantment Hints. Inspired by [Enchantment Descriptions](https://modrinth.com/mod/enchantment-descriptions); not affiliated, no code or text from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/enchantmenthints"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/enchantmenthints/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "enchantmenthints-0.1.0.hnmod"
|
||||
sha256 = "6f1b80a7c22e02b086d5eea2ed6f7c5481b3e40d6a974bf9e382a3df4a2de9df"
|
||||
changelog = "First release."
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
id = "fps_overlay"
|
||||
kind = "mod"
|
||||
kind = "native-mod"
|
||||
name = "FPS Counter"
|
||||
summary = "Frames per second in a corner of the screen, with the minimum and the 1% low."
|
||||
description = """
|
||||
@@ -10,11 +10,24 @@ Horizon's own FPS counter, formerly an extra in Options > Horizon Extras.
|
||||
- Settings: Options > Mods > FPS Counter > Settings (corner; minimum and 1% low).
|
||||
|
||||
It reads the game's own frame counter and draws on the HUD: nothing else.
|
||||
|
||||
Now a native mod (2.0.0): built into the game by the launcher, on the Horizon you play, like any native mod
|
||||
(Settings → Game → Native mods (experimental), then Build). Your settings carry over from 1.0.0. Turn it off in
|
||||
Options > Mods: it rests at once, and the next build leaves it out. Its source is
|
||||
[horizon-mods/native/fps-overlay](https://github.com/horizon-rust/horizon-mods/tree/main/native/fps-overlay).
|
||||
"""
|
||||
authors = ["Horizon"]
|
||||
owners = ["ludovic111"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/horizon-rust/horizon/tree/main/mods/official/fps-overlay"
|
||||
source = "https://github.com/horizon-rust/horizon-mods/tree/main/native/fps-overlay"
|
||||
|
||||
[[versions]]
|
||||
version = "2.0.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "fps_overlay-2.0.0.hnmod"
|
||||
sha256 = "bedb670e2490ce8239906c86eabd3a0c131ef5037b2937607231bdbdba38c171"
|
||||
changelog = "Native: built into the game by the launcher on Horizon's native hook points; settings carry over; turn it off in Options > Mods."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
id = "itemnew"
|
||||
kind = "native-mod"
|
||||
name = "Item New"
|
||||
summary = "Newly picked-up items glow in your hotbar and inventory until you look at them."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
Items you just got (picked up, crafted, taken from a chest, given) get a soft glow in the hotbar and in inventory and container screens, until you point at them or close the screen.
|
||||
|
||||
- Styles: glow, glow with a star, by rarity colour.
|
||||
- Which screens, and when a mark clears, are settings; `/itemnew` shows or clears the marks.
|
||||
|
||||
Settings: Options → Mods → Item New. Inspired by [Item Highlighter](https://modrinth.com/mod/item-highlighter); not affiliated, no code from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/itemnew"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/itemnew/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.1"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "itemnew-0.1.1.hnmod"
|
||||
sha256 = "cdc68c9911736703ebc4b8b81895a719edd74ba9d08fe01ddb34fe09b8d490e9"
|
||||
changelog = "First release."
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
id = "keystrokes"
|
||||
kind = "mod"
|
||||
kind = "native-mod"
|
||||
name = "Keystrokes"
|
||||
summary = "Your movement keys, mouse buttons and space bar in a corner, lit while held, with clicks per second."
|
||||
description = """
|
||||
@@ -10,11 +10,24 @@ Horizon's own keystrokes overlay, formerly an extra in Options > Horizon Extras.
|
||||
- Settings: Options > Mods > Keystrokes > Settings (corner; clicks per second).
|
||||
|
||||
It reads which keys you hold and your clicks, and draws on the HUD: nothing else.
|
||||
|
||||
Now a native mod (2.0.0): built into the game by the launcher, on the Horizon you play, like any native mod
|
||||
(Settings → Game → Native mods (experimental), then Build). Your settings carry over from 1.0.0. Turn it off in
|
||||
Options > Mods: it rests at once, and the next build leaves it out. Its source is
|
||||
[horizon-mods/native/keystrokes](https://github.com/horizon-rust/horizon-mods/tree/main/native/keystrokes).
|
||||
"""
|
||||
authors = ["Horizon"]
|
||||
owners = ["ludovic111"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/horizon-rust/horizon/tree/main/mods/official/keystrokes"
|
||||
source = "https://github.com/horizon-rust/horizon-mods/tree/main/native/keystrokes"
|
||||
|
||||
[[versions]]
|
||||
version = "2.0.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "keystrokes-2.0.0.hnmod"
|
||||
sha256 = "bed34d2f8049e92c52cd7c91c0c4c8c9d165faaf0183500cf4021d9e04783a7d"
|
||||
changelog = "Native: built into the game by the launcher on Horizon's native hook points; settings carry over; turn it off in Options > Mods."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
id = "minimap"
|
||||
kind = "mod"
|
||||
kind = "native-mod"
|
||||
name = "Minimap"
|
||||
summary = "A map of the world around you in a corner of the screen."
|
||||
description = """
|
||||
@@ -10,11 +10,24 @@ Horizon's own minimap, formerly an extra in Options > Horizon Extras.
|
||||
- Settings: Options > Mods > Minimap > Settings (corner; size; square or round; zoom, 0.5 to 4 blocks per pixel; turn with the player).
|
||||
|
||||
It shows a map in a corner of the screen: nothing else.
|
||||
|
||||
Now a native mod (2.0.0): built into the game by the launcher, on the Horizon you play, like any native mod
|
||||
(Settings → Game → Native mods (experimental), then Build). Your settings carry over from 1.0.0. Turn it off in
|
||||
Options > Mods: it rests at once, and the next build leaves it out. Its source is
|
||||
[horizon-mods/native/minimap](https://github.com/horizon-rust/horizon-mods/tree/main/native/minimap).
|
||||
"""
|
||||
authors = ["Horizon"]
|
||||
owners = ["ludovic111"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/horizon-rust/horizon/tree/main/mods/official/minimap"
|
||||
source = "https://github.com/horizon-rust/horizon-mods/tree/main/native/minimap"
|
||||
|
||||
[[versions]]
|
||||
version = "2.0.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "minimap-2.0.0.hnmod"
|
||||
sha256 = "6f5dbca0699562b088a04b3f5abdc1b7faf8da51257a96da0a4ed639c6644ed9"
|
||||
changelog = "Native: built into the game by the launcher on Horizon's native hook points; settings carry over; turn it off in Options > Mods."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
id = "mousetricks"
|
||||
kind = "native-mod"
|
||||
name = "Mouse Tricks"
|
||||
summary = "Inventory mouse tricks: drag to spread or gather, the wheel moves items one by one, and R sorts."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
- **Right-drag**: one item into each slot you pass.
|
||||
- **Left-drag**: gather the same items; with Shift, quick-move many stacks.
|
||||
- **Mouse wheel** over a slot: move items one at a time between your inventory and a container.
|
||||
- **Sort** (R, or middle click on an empty spot): merges partial stacks and sorts the part under the mouse (a chest, or your main inventory; the hotbar only if you ask). Creative-tab order, by name or by count. Items with other names, enchantments or damage never merge; bundles stay put.
|
||||
|
||||
Everything goes through the game's own container clicks, so servers accept it. Each trick is a setting (Options → Mods → Mouse Tricks). Inspired by [Mouse Tweaks](https://modrinth.com/mod/mouse-tweaks); not affiliated, no code from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/mousetricks"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/mousetricks/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.2.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "mousetricks-0.2.0.hnmod"
|
||||
sha256 = "48c60955371963f8e320a9efbe8285bb276d255c29178a8a021e7e2b47535cbe"
|
||||
changelog = "First release."
|
||||
@@ -0,0 +1,27 @@
|
||||
id = "smartf3"
|
||||
kind = "native-mod"
|
||||
name = "SmartF3"
|
||||
summary = "A tidier, configurable F3 debug screen: the same information in modules you can colour, reorder and turn off."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (Settings → Game → Native mods, then Build). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to Horizon 142388e and horizon-mixins 0d6b103.
|
||||
|
||||
The F3 debug screen, grouped into 14 modules: FPS, server, coordinates, chunk, facing, world, biome, light, chart keys, system, rendering, and the targeted block, fluid and entity.
|
||||
|
||||
- Each module: on/off, label and value colours, left or right column, order.
|
||||
- Text shadow, background (vanilla, dark, none), gap between modules, compact layout.
|
||||
- The vanilla F3 is one click (or `/smartf3 off`) away; the F3 charts and keys work as in vanilla.
|
||||
|
||||
Settings: Options → Mods → SmartF3. Inspired by [BetterF3](https://modrinth.com/mod/betterf3); not affiliated, no code from it."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/smartf3"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/smartf3/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "smartf3-0.1.0.hnmod"
|
||||
sha256 = "666fc77a12411c8b525dca3d3d478595e760b73c48b62b936fb6026419547929"
|
||||
changelog = "First release."
|
||||
@@ -0,0 +1,27 @@
|
||||
id = "treefall"
|
||||
kind = "native-mod"
|
||||
name = "TreeFall"
|
||||
summary = "Cut one log of a natural tree with an axe and the whole tree comes down."
|
||||
description = """
|
||||
**A native Horizon mod** (built into the game by the launcher; native mods are not sandboxed, so read the source before you build it).
|
||||
|
||||
Cut one log of a natural tree with an axe and the whole tree comes down. Every log is broken through the game's ordinary mining path, so drops, statistics, hunger and axe wear are exactly as if you had chopped each log yourself.
|
||||
|
||||
- Sneak to cut a single log; log cabins (logs without natural leaves) are never felled.
|
||||
- **Settings page** (Options > Mods > TreeFall): which tools fell (axe / any tool / anything), sneaking, largest tree, leaves needed, cut time (as one log, or longer for bigger trees), tool wear (every log / once / a factor / none), keep durability, hunger, drops at the stump, replant a sapling, break the leaves, huge mushrooms.
|
||||
- `/treefall` on/off per player; LAN guests too.
|
||||
|
||||
In the spirit of FallingTree; not affiliated, no code from it. Pinned to Horizon 142388e and horizon-mixins 0d6b103."""
|
||||
authors = ["d3grando"]
|
||||
owners = ["d3grando"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/d3grando/treefall"
|
||||
icon = "https://raw.githubusercontent.com/d3grando/treefall/main/icon.png"
|
||||
|
||||
[[versions]]
|
||||
version = "0.3.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "treefall-0.3.0.hnmod"
|
||||
sha256 = "6f7feea8d0f55291953476b272f00cfc4be42d64972e7efefeb3df47c9dae8ce"
|
||||
changelog = "First registry release. A settings page in Options > Mods; the cut can take longer for bigger trees (and letting go cancels it, in single player too); trees come down from the top and leave a stump when the axe would wear out; replanting, drops at the stump, leaves and huge mushrooms are options."
|
||||
+15
-2
@@ -1,5 +1,5 @@
|
||||
id = "zoom"
|
||||
kind = "mod"
|
||||
kind = "native-mod"
|
||||
name = "Zoom"
|
||||
summary = "Hold a key to zoom in; the mouse wheel zooms further while you hold it."
|
||||
description = """
|
||||
@@ -11,11 +11,24 @@ Horizon's own zoom, formerly an extra in Options > Horizon Extras.
|
||||
- Settings: Options > Mods > Zoom > Settings (the key; starting zoom; smooth zoom; slower turning).
|
||||
|
||||
It hears its own key and the mouse wheel while you hold it, and zooms the camera: nothing else.
|
||||
|
||||
Now a native mod (2.0.0): built into the game by the launcher, on the Horizon you play, like any native mod
|
||||
(Settings → Game → Native mods (experimental), then Build). Your settings carry over from 1.0.0. Turn it off in
|
||||
Options > Mods: it rests at once, and the next build leaves it out. Its source is
|
||||
[horizon-mods/native/zoom](https://github.com/horizon-rust/horizon-mods/tree/main/native/zoom).
|
||||
"""
|
||||
authors = ["Horizon"]
|
||||
owners = ["ludovic111"]
|
||||
license = "MIT"
|
||||
source = "https://github.com/horizon-rust/horizon/tree/main/mods/official/zoom"
|
||||
source = "https://github.com/horizon-rust/horizon-mods/tree/main/native/zoom"
|
||||
|
||||
[[versions]]
|
||||
version = "2.0.0"
|
||||
published = "2026-10-29"
|
||||
game = "26.3"
|
||||
file = "zoom-2.0.0.hnmod"
|
||||
sha256 = "49fa2d7d635225c53304fbf1477606c0677eb23d94dac75b730f6c3ba065f127"
|
||||
changelog = "Native: built into the game by the launcher on Horizon's native hook points; settings carry over; turn it off in Options > Mods."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"game": "26.3",
|
||||
"hooks": 1,
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_revision": "1149c92cb8c1fde5136197b6647a6eb220401a60",
|
||||
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ priority = 100
|
||||
# The commit it was tested on. It uses only the native hook points (hooks 1 in
|
||||
# compatibility.json): the launcher builds it on any Horizon that has them.
|
||||
[compatibility]
|
||||
horizon_revision = "1149c92cb8c1fde5136197b6647a6eb220401a60"
|
||||
horizon_revision = "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
|
||||
@@ -6,6 +6,10 @@ fn fps_hud(
|
||||
corners: &mut horizon_client::extras_hud::Corners,
|
||||
) {
|
||||
use crate::game::official_fps_overlay as m;
|
||||
// Turned off in Options > Mods: nothing until the next build leaves it out.
|
||||
if !game.native_on(m::ID) {
|
||||
return;
|
||||
}
|
||||
use horizon_client::extras::Corner;
|
||||
let settings = {
|
||||
let game = &*game;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"game": "26.3",
|
||||
"hooks": 1,
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_revision": "1149c92cb8c1fde5136197b6647a6eb220401a60",
|
||||
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ priority = 100
|
||||
# The commit it was tested on. It uses only the native hook points (hooks 1 in
|
||||
# compatibility.json): the launcher builds it on any Horizon that has them.
|
||||
[compatibility]
|
||||
horizon_revision = "1149c92cb8c1fde5136197b6647a6eb220401a60"
|
||||
horizon_revision = "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
|
||||
@@ -6,6 +6,10 @@ fn keystrokes_hud(
|
||||
corners: &mut horizon_client::extras_hud::Corners,
|
||||
) {
|
||||
use crate::game::official_keystrokes as m;
|
||||
// Turned off in Options > Mods: nothing until the next build leaves it out.
|
||||
if !game.native_on(m::ID) {
|
||||
return;
|
||||
}
|
||||
use horizon_client::extras::Corner;
|
||||
let settings = {
|
||||
let game = &*game;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"game": "26.3",
|
||||
"hooks": 1,
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_revision": "1149c92cb8c1fde5136197b6647a6eb220401a60",
|
||||
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ priority = 100
|
||||
# The commit it was tested on. It uses only the native hook points (hooks 1 in
|
||||
# compatibility.json): the launcher builds it on any Horizon that has them.
|
||||
[compatibility]
|
||||
horizon_revision = "1149c92cb8c1fde5136197b6647a6eb220401a60"
|
||||
horizon_revision = "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
fn minimap_view(game: &crate::game::Game<'_>, minimap: &mut Option<crate::game::native_hooks::NativeMinimap>) {
|
||||
use crate::game::official_minimap as m;
|
||||
use horizon_client::extras::Corner;
|
||||
if minimap.is_some() {
|
||||
// Turned off in Options > Mods: nothing until the next build leaves it out.
|
||||
if minimap.is_some() || !game.native_on(m::ID) {
|
||||
return;
|
||||
}
|
||||
let settings = m::settings(|key| game.native_setting(m::ID, key));
|
||||
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "horizon-mod-recipes"
|
||||
version = "2.0.0"
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "horizon-mod-recipes"
|
||||
version = "2.0.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
description = "Horizon's official Recipes viewer, a native mod: every item beside your inventory, its recipes and uses."
|
||||
|
||||
# The mod's logic and its tests: the same file the bundle adds to the game.
|
||||
[lib]
|
||||
path = "mixins/recipes/runtime.rs"
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Ludovic Marie and the Horizon contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Recipes (Horizon native mod)
|
||||
|
||||
Horizon's official recipe viewer: every item beside your inventory, with a search box (`#tag` searches a tag). R over an
|
||||
item shows its recipes, one tab per station; U shows its uses. The + under a crafting recipe you know fills the
|
||||
crafting grid, as the recipe book does. In creative mode, turn on "Click gives items" (Options > Mods > Recipes >
|
||||
Settings): a click on an item in the panel gives you one, a right click a stack. Change R and U on the mod's page in
|
||||
Options > Mods.
|
||||
|
||||
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/recipes/`) that the Horizon
|
||||
launcher builds into the game. It replaces the WebAssembly Recipes 1.x and keeps its settings: the same file
|
||||
(`horizon-mods/.horizon-mod-data/recipes/give`) and the same keys (`key.recipes.recipe`, `key.recipes.uses` in
|
||||
`horizon-mods.txt`).
|
||||
|
||||
## Install
|
||||
|
||||
In the Horizon launcher: Mods → Recipes → Install, then Settings → Game → Native mods (experimental) → Build. The
|
||||
launcher downloads its own Rust and builds the game with your native mods; see Horizon's
|
||||
[native mods](https://github.com/horizon-rust/horizon/blob/main/docs/native-mods.md).
|
||||
|
||||
## Made on the hook points
|
||||
|
||||
The bundle hooks only Horizon's native hook points (`crates/horizon-client/src/game/native_hooks.rs`), which keep
|
||||
their names and signatures across releases. It needs the item panel and the recipe tables, so `compatibility.json`
|
||||
names version 2 (`"hooks": 2`): the launcher builds Recipes on any Horizon with that version or a later one, the
|
||||
player's own game by default, together with any other mod. `horizon_revision` is only the commit it was last tested
|
||||
on.
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `mixins/recipes/horizon-mixin.toml` | The bundle: two modules and the HEAD hooks |
|
||||
| `mixins/recipes/runtime.rs` | The viewer: search, pages per station, history, and its tests; added to the game as `game::official_recipes`, and this crate's library |
|
||||
| `mixins/recipes/glue.rs` | The viewer's edges in the game (`game::official_recipes_glue`): the game's tables and panel events in, the panel, give and place out |
|
||||
| `mixins/recipes/*.rs` | One small helper per hook |
|
||||
| `proof.py` (with `../tools/game.py`) | The proof: the built game driven with `horizon-ctl --offscreen` on fresh, temporary data |
|
||||
|
||||
## Build and prove
|
||||
|
||||
```sh
|
||||
(cd native/recipes && cargo test) # the viewer
|
||||
python3 native/tools/native.py check recipes # apply on the tested Horizon and cargo check the game
|
||||
python3 native/tools/native.py check all # with the other native mods, as a player's set
|
||||
python3 native/tools/native.py package recipes # native/target/recipes-<version>.hnmod for the registry
|
||||
python3 native/tools/native.py proof recipes --resources /path/to/.local/horizon-client/26.3
|
||||
```
|
||||
|
||||
The proof needs resources prepared from your own Minecraft 26.3 (`horizon-prepare`); none are in this repository.
|
||||
It seeds the WebAssembly mod's setting ("Click gives items" on) in a temporary game folder, checks that Options > Mods
|
||||
shows the mod, its keys and the setting, then opens the inventory: the panel shows items, a click on one in creative
|
||||
mode gives it, and with the setting turned off the same click opens its recipe page.
|
||||
|
||||
License: MIT.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"game": "26.3",
|
||||
"hooks": 2,
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_revision": "8f91683323ac1165e7d76b3037e18f30fe647205",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// A click on the mod's settings page: "Click gives items" flips, kept where
|
||||
// the WebAssembly mod kept it.
|
||||
fn official_recipes_click(game: &mut crate::game::Game<'_>, mod_id: &str, button: usize) {
|
||||
use crate::game::official_recipes as m;
|
||||
if mod_id != m::ID || button != 0 {
|
||||
return;
|
||||
}
|
||||
let give = !m::gives(game.native_setting(m::ID, "give"));
|
||||
game.native_set_setting(m::ID, "give", if give { "true" } else { "false" });
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// The player's click, search or key on the panel, when it is this mod's.
|
||||
fn official_recipes_event(
|
||||
game: &mut crate::game::Game<'_>,
|
||||
mod_id: &str,
|
||||
event: &crate::game::native_hooks::NativePanelEvent,
|
||||
) {
|
||||
use crate::game::official_recipes as m;
|
||||
if mod_id != m::ID || !game.native_on(m::ID) {
|
||||
return;
|
||||
}
|
||||
crate::game::official_recipes_glue::event(game, event);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Each frame: the item panel follows the world and the mod's on/off.
|
||||
fn official_recipes_frame(game: &mut crate::game::Game<'_>) {
|
||||
crate::game::official_recipes_glue::frame(game);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// The viewer's edges in the game (not in the Cargo library): the game's
|
||||
// tables and events converted to the viewer's types, and what the viewer
|
||||
// asks for done through the native hook points (API 2).
|
||||
use crate::game::Game;
|
||||
use crate::game::native_hooks::{
|
||||
NativeItem, NativePanelEvent, NativeRecipe, NativeRecipeKind, NativeRecipeLayout,
|
||||
NativeRecipeStack, NativeRecipeView, NativeStack,
|
||||
};
|
||||
use crate::game::official_recipes as m;
|
||||
|
||||
fn item(i: &NativeItem) -> m::Item {
|
||||
m::Item {
|
||||
id: i.id.clone(),
|
||||
name: i.name.clone(),
|
||||
tags: i.tags.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ingredient(s: &NativeRecipeStack) -> m::Ingredient {
|
||||
m::Ingredient {
|
||||
item: s.item,
|
||||
count: s.count,
|
||||
components: s.components.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn recipe(r: &NativeRecipe) -> m::Recipe {
|
||||
let kind = match r.kind {
|
||||
NativeRecipeKind::CraftingShaped => m::Kind::CraftingShaped,
|
||||
NativeRecipeKind::CraftingShapeless => m::Kind::CraftingShapeless,
|
||||
NativeRecipeKind::Smelting => m::Kind::Smelting,
|
||||
NativeRecipeKind::Blasting => m::Kind::Blasting,
|
||||
NativeRecipeKind::Smoking => m::Kind::Smoking,
|
||||
NativeRecipeKind::Campfire => m::Kind::Campfire,
|
||||
NativeRecipeKind::Stonecutting => m::Kind::Stonecutting,
|
||||
NativeRecipeKind::Smithing => m::Kind::Smithing,
|
||||
NativeRecipeKind::Brewing => m::Kind::Brewing,
|
||||
};
|
||||
m::Recipe {
|
||||
kind,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
slots: r
|
||||
.slots
|
||||
.iter()
|
||||
.map(|s| s.iter().map(ingredient).collect())
|
||||
.collect(),
|
||||
results: r.results.iter().map(ingredient).collect(),
|
||||
cooking_time: r.cooking_time,
|
||||
experience: r.experience,
|
||||
book_id: r.book_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn native(s: &m::Stack) -> NativeStack {
|
||||
NativeStack {
|
||||
item: s.item.clone(),
|
||||
count: s.count,
|
||||
components: s.components.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stack(s: &NativeStack) -> m::Stack {
|
||||
m::Stack {
|
||||
item: s.item.clone(),
|
||||
count: s.count,
|
||||
components: s.components.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(v: &m::View) -> NativeRecipeView {
|
||||
NativeRecipeView {
|
||||
layout: match v.layout {
|
||||
m::Layout::Grid => NativeRecipeLayout::Grid,
|
||||
m::Layout::Cooking => NativeRecipeLayout::Cooking,
|
||||
m::Layout::Single => NativeRecipeLayout::Single,
|
||||
m::Layout::Smithing => NativeRecipeLayout::Smithing,
|
||||
m::Layout::Brewing => NativeRecipeLayout::Brewing,
|
||||
},
|
||||
width: v.width,
|
||||
height: v.height,
|
||||
slots: v
|
||||
.slots
|
||||
.iter()
|
||||
.map(|s| s.iter().map(native).collect())
|
||||
.collect(),
|
||||
results: v.results.iter().map(native).collect(),
|
||||
note: v.note.clone(),
|
||||
action: v.action,
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the world's tables once a world is open, and drops them (and the
|
||||
/// panel) when it closes or the player turns the mod off.
|
||||
pub(super) fn frame(game: &mut Game<'_>) {
|
||||
let tables = if game.native_on(m::ID) {
|
||||
game.native_recipes()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
match (tables, m::loaded()) {
|
||||
(Some(t), false) => {
|
||||
let (viewer, out) = m::Viewer::new(
|
||||
t.items.iter().map(item).collect(),
|
||||
t.recipes.iter().map(recipe).collect(),
|
||||
);
|
||||
m::set_viewer(Some(viewer));
|
||||
apply(game, out);
|
||||
}
|
||||
(None, true) => {
|
||||
m::set_viewer(None);
|
||||
game.native_panel_recipes(m::ID, "", Vec::new(), 0, Vec::new());
|
||||
game.native_panel_items(m::ID, Vec::new());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// The player's action on the panel, as the WebAssembly mod handled it.
|
||||
pub(super) fn event(game: &mut Game<'_>, event: &NativePanelEvent) {
|
||||
let event = match event {
|
||||
NativePanelEvent::Search(text) => m::Event::Search(text.clone()),
|
||||
NativePanelEvent::Clicked { index, right } => m::Event::Clicked {
|
||||
index: *index as usize,
|
||||
right: *right,
|
||||
},
|
||||
NativePanelEvent::Key { key, stack: s } => {
|
||||
let [[recipe, _, recipe_default], [uses, _, uses_default]] = m::KEYS;
|
||||
let uses = if *key == game.native_mod_key(m::ID, recipe, recipe_default) {
|
||||
false
|
||||
} else if *key == game.native_mod_key(m::ID, uses, uses_default) {
|
||||
true
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
m::Event::Key {
|
||||
uses,
|
||||
stack: stack(s),
|
||||
}
|
||||
}
|
||||
NativePanelEvent::SlotClicked {
|
||||
stack: s, right, ..
|
||||
} => m::Event::SlotClicked {
|
||||
stack: stack(s),
|
||||
right: *right,
|
||||
},
|
||||
NativePanelEvent::Tab(i) => m::Event::Tab(*i as usize),
|
||||
NativePanelEvent::Action(i) => m::Event::Action(*i as usize),
|
||||
NativePanelEvent::Back => m::Event::Back,
|
||||
_ => return,
|
||||
};
|
||||
let give = m::gives(game.native_setting(m::ID, "give"));
|
||||
let known = game.native_known_recipes();
|
||||
let outs = m::with_viewer(|v| v.handle(event.clone(), give, true, &known)).unwrap_or_default();
|
||||
for out in outs {
|
||||
if let m::Out::Give { item, count } = &out {
|
||||
// Not in creative mode: the click shows the item's page, as with the setting off.
|
||||
if game.native_give(item, *count).is_err() {
|
||||
let again = m::with_viewer(|v| v.handle(event.clone(), false, false, &known));
|
||||
for out in again.unwrap_or_default() {
|
||||
apply(game, out);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
apply(game, out);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply(game: &mut Game<'_>, out: m::Out) {
|
||||
match out {
|
||||
m::Out::Items(items) => game.native_panel_items(m::ID, items.iter().map(native).collect()),
|
||||
m::Out::Recipes {
|
||||
title,
|
||||
tabs,
|
||||
tab,
|
||||
views,
|
||||
} => game.native_panel_recipes(
|
||||
m::ID,
|
||||
&title,
|
||||
tabs.iter().map(native).collect(),
|
||||
tab as u32,
|
||||
views.iter().map(view).collect(),
|
||||
),
|
||||
m::Out::Give { item, count } => {
|
||||
let _ = game.native_give(&item, count);
|
||||
}
|
||||
m::Out::Place(book) => {
|
||||
if let Err(e) = game.native_place_recipe(book, false) {
|
||||
game.native_chat(&e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
[mod]
|
||||
id = "recipes"
|
||||
version = "2.0.0"
|
||||
priority = 100
|
||||
|
||||
# The commit it was tested on. It uses only the native hook points (hooks 2 in
|
||||
# compatibility.json): the launcher builds it on any Horizon that has them.
|
||||
[compatibility]
|
||||
horizon_revision = "8f91683323ac1165e7d76b3037e18f30fe647205"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
file = "crates/horizon-client/src/game.rs"
|
||||
name = "official_recipes"
|
||||
source = "runtime.rs"
|
||||
|
||||
[[module]]
|
||||
id = "glue"
|
||||
file = "crates/horizon-client/src/game.rs"
|
||||
name = "official_recipes_glue"
|
||||
source = "glue.rs"
|
||||
|
||||
[[injection]]
|
||||
id = "list"
|
||||
kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_mods"
|
||||
helper = "list.rs"
|
||||
bindings = [
|
||||
{ name = "mods", ty = "&mut Vec<crate::game::native_hooks::NativeMod>", value = "&mut *mods" },
|
||||
]
|
||||
|
||||
[[injection]]
|
||||
id = "settings"
|
||||
kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_settings"
|
||||
helper = "settings.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
|
||||
{ name = "mod_id", ty = "&str", value = "mod_id" },
|
||||
{ name = "buttons", ty = "&mut Vec<String>", value = "&mut *buttons" },
|
||||
]
|
||||
|
||||
[[injection]]
|
||||
id = "click"
|
||||
kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_settings_click"
|
||||
helper = "click.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
|
||||
{ name = "mod_id", ty = "&str", value = "mod_id" },
|
||||
{ name = "button", ty = "usize", value = "button" },
|
||||
]
|
||||
|
||||
[[injection]]
|
||||
id = "frame"
|
||||
kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_frame"
|
||||
helper = "frame.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
|
||||
]
|
||||
|
||||
[[injection]]
|
||||
id = "event"
|
||||
kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_panel_event"
|
||||
helper = "event.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
|
||||
{ name = "mod_id", ty = "&str", value = "mod_id" },
|
||||
{ name = "event", ty = "&crate::game::native_hooks::NativePanelEvent", value = "event" },
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
// The mod's row in Options > Mods, with its two keys.
|
||||
fn official_recipes_listed(mods: &mut Vec<crate::game::native_hooks::NativeMod>) {
|
||||
use crate::game::official_recipes as m;
|
||||
mods.push(crate::game::native_hooks::NativeMod {
|
||||
id: m::ID.into(),
|
||||
title: format!("{} {}", m::NAME, m::VERSION),
|
||||
description: m::DESCRIPTION.into(),
|
||||
keys: m::KEYS.iter().map(|k| k.map(String::from)).collect(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,669 @@
|
||||
// Horizon's official recipe viewer, formerly the WebAssembly mod: every item
|
||||
// in the panel the game draws beside container screens, with a search box. R
|
||||
// over an item, the panel's or any slot's, shows its recipes, U its uses; a
|
||||
// click does the same, left for recipes and right for uses. A recipe page has
|
||||
// a tab per station, and + beside a crafting recipe the player knows fills the
|
||||
// grid as the recipe book does. In creative mode, with "Click gives items" on,
|
||||
// a click on a panel item gives one, a right click a stack. Its setting and
|
||||
// keys are the WebAssembly mod's (`horizon-mods/.horizon-mod-data/recipes/give`,
|
||||
// `key.recipes.recipe` and `key.recipes.uses` in horizon-mods.txt), so a
|
||||
// player keeps them. This file is both the mod's Cargo library (its tests) and
|
||||
// the module the bundle adds to the game; the game's types are converted at
|
||||
// the edges (frame.rs, event.rs).
|
||||
|
||||
/// The mod's id (its settings folder), name and version.
|
||||
pub const ID: &str = "recipes";
|
||||
pub const NAME: &str = "Recipes";
|
||||
pub const VERSION: &str = "2.0.0";
|
||||
pub const DESCRIPTION: &str = "Every item beside your inventory, with a search box. R over an item shows its recipes, U its uses, and + fills the crafting grid with a recipe you know. Horizon's own.";
|
||||
/// Its keys: name, label, default (the WebAssembly mod's).
|
||||
pub const KEYS: [[&str; 3]; 2] = [
|
||||
["recipe", "Show recipes", "key.keyboard.r"],
|
||||
["uses", "Show uses", "key.keyboard.u"],
|
||||
];
|
||||
/// What a recipe page holds at most, as the panel takes.
|
||||
pub const MAX_RECIPES: usize = 256;
|
||||
pub const MAX_STACKS: usize = 64;
|
||||
/// A right click's gift in creative mode (cut to one stack by the game).
|
||||
pub const STACK: u32 = 99;
|
||||
/// The stations, in the order of their tabs, with each one's icon.
|
||||
pub const STATIONS: [&str; 8] = [
|
||||
"minecraft:crafting_table",
|
||||
"minecraft:furnace",
|
||||
"minecraft:blast_furnace",
|
||||
"minecraft:smoker",
|
||||
"minecraft:campfire",
|
||||
"minecraft:stonecutter",
|
||||
"minecraft:smithing_table",
|
||||
"minecraft:brewing_stand",
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
CraftingShaped,
|
||||
CraftingShapeless,
|
||||
Smelting,
|
||||
Blasting,
|
||||
Smoking,
|
||||
Campfire,
|
||||
Stonecutting,
|
||||
Smithing,
|
||||
Brewing,
|
||||
}
|
||||
|
||||
fn station(kind: Kind) -> usize {
|
||||
match kind {
|
||||
Kind::CraftingShaped | Kind::CraftingShapeless => 0,
|
||||
Kind::Smelting => 1,
|
||||
Kind::Blasting => 2,
|
||||
Kind::Smoking => 3,
|
||||
Kind::Campfire => 4,
|
||||
Kind::Stonecutting => 5,
|
||||
Kind::Smithing => 6,
|
||||
Kind::Brewing => 7,
|
||||
}
|
||||
}
|
||||
|
||||
/// An item the game knows.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Item {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// An item of the tables by its index, as recipes name them.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Ingredient {
|
||||
pub item: u32,
|
||||
pub count: u32,
|
||||
pub components: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Recipe {
|
||||
pub kind: Kind,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub slots: Vec<Vec<Ingredient>>,
|
||||
pub results: Vec<Ingredient>,
|
||||
pub cooking_time: u32,
|
||||
pub experience: f32,
|
||||
/// The recipe book's id, or -1.
|
||||
pub book_id: i32,
|
||||
}
|
||||
|
||||
/// An item as the panel shows it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Stack {
|
||||
pub item: String,
|
||||
pub count: u32,
|
||||
pub components: String,
|
||||
}
|
||||
|
||||
fn stack(item: &str) -> Stack {
|
||||
Stack {
|
||||
item: item.into(),
|
||||
count: 1,
|
||||
components: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Grid,
|
||||
Cooking,
|
||||
Single,
|
||||
Smithing,
|
||||
Brewing,
|
||||
}
|
||||
|
||||
/// A recipe as the panel lays it out.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct View {
|
||||
pub layout: Layout,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub slots: Vec<Vec<Stack>>,
|
||||
pub results: Vec<Stack>,
|
||||
pub note: String,
|
||||
/// + beside it: none, or whether the book holds it.
|
||||
pub action: Option<bool>,
|
||||
}
|
||||
|
||||
/// What the player did on the panel.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Event {
|
||||
Search(String),
|
||||
/// A panel item, by its index in the items shown.
|
||||
Clicked {
|
||||
index: usize,
|
||||
right: bool,
|
||||
},
|
||||
/// The recipes key (`uses` false) or the uses key over an item.
|
||||
Key {
|
||||
uses: bool,
|
||||
stack: Stack,
|
||||
},
|
||||
/// A slot of a recipe shown.
|
||||
SlotClicked {
|
||||
stack: Stack,
|
||||
right: bool,
|
||||
},
|
||||
Tab(usize),
|
||||
Action(usize),
|
||||
Back,
|
||||
}
|
||||
|
||||
/// What the game is to do.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Out {
|
||||
/// Show these items in the panel.
|
||||
Items(Vec<Stack>),
|
||||
/// Show this recipe page; an empty one goes back to the items.
|
||||
Recipes {
|
||||
title: String,
|
||||
tabs: Vec<Stack>,
|
||||
tab: usize,
|
||||
views: Vec<View>,
|
||||
},
|
||||
/// Creative mode: give the player this item.
|
||||
Give { item: String, count: u32 },
|
||||
/// Place this recipe book id in the open grid.
|
||||
Place(i32),
|
||||
}
|
||||
|
||||
/// A recipe page: an item's recipes or its uses, on one station's tab.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct Page {
|
||||
item: Stack,
|
||||
uses: bool,
|
||||
tab: usize,
|
||||
}
|
||||
|
||||
/// The viewer's state while a world is open.
|
||||
#[derive(Default)]
|
||||
pub struct Viewer {
|
||||
items: Vec<Item>,
|
||||
all: Vec<Recipe>,
|
||||
/// The panel's items now: indexes into `items`, after the search.
|
||||
listed: Vec<usize>,
|
||||
search: String,
|
||||
/// The page shown, then the ones Back returns to.
|
||||
history: Vec<Page>,
|
||||
/// The book id behind each recipe the page shows (-1: none).
|
||||
shown: Vec<i32>,
|
||||
}
|
||||
|
||||
impl Viewer {
|
||||
/// The world's tables: the panel lists every item.
|
||||
pub fn new(items: Vec<Item>, all: Vec<Recipe>) -> (Self, Out) {
|
||||
let mut me = Viewer {
|
||||
items,
|
||||
all,
|
||||
..Viewer::default()
|
||||
};
|
||||
let out = me.list();
|
||||
(me, out)
|
||||
}
|
||||
|
||||
fn id(&self, s: &Ingredient) -> &str {
|
||||
self.items
|
||||
.get(s.item as usize)
|
||||
.map_or("minecraft:air", |i| &i.id)
|
||||
}
|
||||
fn convert(&self, s: &Ingredient) -> Stack {
|
||||
Stack {
|
||||
item: self.id(s).into(),
|
||||
count: s.count,
|
||||
components: s.components.clone(),
|
||||
}
|
||||
}
|
||||
/// The same item, and the same components when `want` names some.
|
||||
fn matches(&self, s: &Ingredient, want: &Stack) -> bool {
|
||||
self.id(s) == want.item && (want.components.is_empty() || s.components == want.components)
|
||||
}
|
||||
fn name<'a>(&'a self, item: &'a str) -> &'a str {
|
||||
self.items
|
||||
.iter()
|
||||
.find(|i| i.id == item)
|
||||
.map_or(item, |i| &i.name)
|
||||
}
|
||||
|
||||
/// The items the search keeps: every word must be in the name or the
|
||||
/// id; `@minecraft` keeps a namespace, `#logs` a tag.
|
||||
fn list(&mut self) -> Out {
|
||||
let words: Vec<String> = self
|
||||
.search
|
||||
.to_lowercase()
|
||||
.split_whitespace()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
let keeps = |item: &Item| {
|
||||
words.iter().all(|w| {
|
||||
if let Some(ns) = w.strip_prefix('@') {
|
||||
item.id.split(':').next().is_some_and(|n| n.starts_with(ns))
|
||||
} else if let Some(tag) = w.strip_prefix('#') {
|
||||
item.tags.iter().any(|t| t.contains(tag))
|
||||
} else {
|
||||
item.name.to_lowercase().contains(w.as_str()) || item.id.contains(w.as_str())
|
||||
}
|
||||
})
|
||||
};
|
||||
self.listed = (self.items.iter().enumerate())
|
||||
.filter(|(_, i)| i.id != "minecraft:air" && keeps(i))
|
||||
.map(|(n, _)| n)
|
||||
.collect();
|
||||
Out::Items(
|
||||
self.listed
|
||||
.iter()
|
||||
.map(|&n| stack(&self.items[n].id))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// The recipes a page lists, all stations together.
|
||||
fn found(&self, page: &Page) -> Vec<&Recipe> {
|
||||
(self.all.iter())
|
||||
.filter(|r| {
|
||||
if page.uses {
|
||||
r.slots
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|s| self.matches(s, &page.item))
|
||||
} else {
|
||||
r.results.iter().any(|s| self.matches(s, &page.item))
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Opens a page, pushing it on the history; nothing when it is empty.
|
||||
fn open(&mut self, item: Stack, uses: bool, known: &[i32]) -> Option<Out> {
|
||||
let item = Stack { count: 1, ..item };
|
||||
let page = Page { item, uses, tab: 0 };
|
||||
if self.found(&page).is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.history.push(page);
|
||||
Some(self.show(known))
|
||||
}
|
||||
|
||||
/// The page on top of the history, or no page when there is none.
|
||||
fn show(&mut self, known: &[i32]) -> Out {
|
||||
let Some(mut page) = self.history.last().cloned() else {
|
||||
self.shown.clear();
|
||||
return Out::Recipes {
|
||||
title: String::new(),
|
||||
tabs: Vec::new(),
|
||||
tab: 0,
|
||||
views: Vec::new(),
|
||||
};
|
||||
};
|
||||
let found = self.found(&page);
|
||||
let stations: Vec<usize> = (0..STATIONS.len())
|
||||
.filter(|&s| found.iter().any(|r| station(r.kind) == s))
|
||||
.collect();
|
||||
page.tab = page.tab.min(stations.len().saturating_sub(1));
|
||||
let on_tab = |r: &&&Recipe| stations.get(page.tab) == Some(&station(r.kind));
|
||||
let on: Vec<&Recipe> = found
|
||||
.iter()
|
||||
.filter(on_tab)
|
||||
.copied()
|
||||
.take(MAX_RECIPES)
|
||||
.collect();
|
||||
let total = found.iter().filter(on_tab).count();
|
||||
let views = on.iter().map(|r| self.view(r, known)).collect();
|
||||
let mut title = format!(
|
||||
"{}: {}",
|
||||
if page.uses { "Uses" } else { "Recipes" },
|
||||
self.name(&page.item.item)
|
||||
);
|
||||
if total > on.len() {
|
||||
title.push_str(&format!(" ({} of {total})", on.len()));
|
||||
}
|
||||
let tabs = stations.iter().map(|&s| stack(STATIONS[s])).collect();
|
||||
self.shown = on.iter().map(|r| r.book_id).collect();
|
||||
if let Some(top) = self.history.last_mut() {
|
||||
top.tab = page.tab;
|
||||
}
|
||||
Out::Recipes {
|
||||
title,
|
||||
tabs,
|
||||
tab: page.tab,
|
||||
views,
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, r: &Recipe, known: &[i32]) -> View {
|
||||
let mut slots: Vec<Vec<Stack>> = (r.slots.iter())
|
||||
.map(|s| s.iter().take(MAX_STACKS).map(|s| self.convert(s)).collect())
|
||||
.collect();
|
||||
let results = (r.results.iter().take(MAX_STACKS))
|
||||
.map(|s| self.convert(s))
|
||||
.collect();
|
||||
let (layout, width, height, note) = match r.kind {
|
||||
Kind::CraftingShaped => (Layout::Grid, r.width, r.height, String::new()),
|
||||
Kind::CraftingShapeless => {
|
||||
let n = slots.len() as u32;
|
||||
let width = match n {
|
||||
0 | 1 => 1,
|
||||
2..=4 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
let height = n.div_ceil(width).max(1);
|
||||
slots.resize((width * height) as usize, Vec::new());
|
||||
(Layout::Grid, width, height, "Shapeless".into())
|
||||
}
|
||||
Kind::Smelting | Kind::Blasting | Kind::Smoking | Kind::Campfire => {
|
||||
let seconds = r.cooking_time as f32 / 20.0;
|
||||
let note = format!("{seconds} s, {} XP", (r.experience * 100.0).round() / 100.0);
|
||||
(Layout::Cooking, 1, 1, note)
|
||||
}
|
||||
Kind::Stonecutting => (Layout::Single, 1, 1, String::new()),
|
||||
Kind::Smithing => (Layout::Smithing, 3, 1, String::new()),
|
||||
Kind::Brewing => (Layout::Brewing, 2, 1, String::new()),
|
||||
};
|
||||
// + only for what the recipe book shows; greyed until the book has it.
|
||||
let action = (r.book_id >= 0).then(|| known.contains(&r.book_id));
|
||||
View {
|
||||
layout,
|
||||
width,
|
||||
height,
|
||||
slots,
|
||||
results,
|
||||
note,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
/// The player's action; `give` is the setting, `creative` whether the
|
||||
/// game would give. `known` is the recipe book's ids now.
|
||||
pub fn handle(&mut self, event: Event, give: bool, creative: bool, known: &[i32]) -> Vec<Out> {
|
||||
match event {
|
||||
Event::Search(text) => {
|
||||
self.search = text;
|
||||
vec![self.list()]
|
||||
}
|
||||
Event::Clicked { index, right } => {
|
||||
let Some(&n) = self.listed.get(index) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let item = self.items[n].id.clone();
|
||||
if give && creative {
|
||||
let count = if right { STACK } else { 1 };
|
||||
return vec![Out::Give { item, count }];
|
||||
}
|
||||
self.open(stack(&item), right, known).into_iter().collect()
|
||||
}
|
||||
Event::Key { uses, stack } => self.open(stack, uses, known).into_iter().collect(),
|
||||
Event::SlotClicked { stack, right } => {
|
||||
self.open(stack, right, known).into_iter().collect()
|
||||
}
|
||||
Event::Tab(i) => {
|
||||
if let Some(top) = self.history.last_mut() {
|
||||
top.tab = i;
|
||||
}
|
||||
vec![self.show(known)]
|
||||
}
|
||||
Event::Action(i) => match self.shown.get(i) {
|
||||
Some(&book) if book >= 0 => vec![Out::Place(book)],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
Event::Back => {
|
||||
self.history.pop();
|
||||
vec![self.show(known)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// "Click gives items" from its stored value (the WebAssembly mod's
|
||||
/// `true`/`false`); off by default.
|
||||
pub fn gives(stored: Option<String>) -> bool {
|
||||
stored.is_some_and(|v| v.trim() == "true")
|
||||
}
|
||||
|
||||
/// The settings page's button.
|
||||
pub fn give_label(give: bool) -> String {
|
||||
format!(
|
||||
"Click gives items (creative): {}",
|
||||
if give { "ON" } else { "OFF" }
|
||||
)
|
||||
}
|
||||
|
||||
std::thread_local! {
|
||||
/// The viewer while a world is open; the game is one thread.
|
||||
static VIEWER: std::cell::RefCell<Option<Viewer>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Runs `f` on the viewer, if a world is open.
|
||||
pub fn with_viewer<R>(f: impl FnOnce(&mut Viewer) -> R) -> Option<R> {
|
||||
VIEWER.with(|v| v.borrow_mut().as_mut().map(f))
|
||||
}
|
||||
|
||||
/// Whether the viewer holds a world's tables.
|
||||
pub fn loaded() -> bool {
|
||||
VIEWER.with(|v| v.borrow().is_some())
|
||||
}
|
||||
|
||||
/// Keeps the viewer of a world just opened (or none when it closed).
|
||||
pub fn set_viewer(viewer: Option<Viewer>) {
|
||||
VIEWER.with(|v| *v.borrow_mut() = viewer);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tables() -> (Vec<Item>, Vec<Recipe>) {
|
||||
let item = |id: &str, name: &str, tags: &[&str]| Item {
|
||||
id: id.into(),
|
||||
name: name.into(),
|
||||
tags: tags.iter().map(|t| t.to_string()).collect(),
|
||||
};
|
||||
let items = vec![
|
||||
item("minecraft:air", "Air", &[]),
|
||||
item("minecraft:oak_log", "Oak Log", &["minecraft:logs"]),
|
||||
item("minecraft:oak_planks", "Oak Planks", &["minecraft:planks"]),
|
||||
item("minecraft:stick", "Stick", &[]),
|
||||
item("minecraft:charcoal", "Charcoal", &[]),
|
||||
];
|
||||
let one = |item| Ingredient {
|
||||
item,
|
||||
count: 1,
|
||||
components: String::new(),
|
||||
};
|
||||
let planks = Recipe {
|
||||
kind: Kind::CraftingShapeless,
|
||||
width: 0,
|
||||
height: 0,
|
||||
slots: vec![vec![one(1)]],
|
||||
results: vec![Ingredient { count: 4, ..one(2) }],
|
||||
cooking_time: 0,
|
||||
experience: 0.0,
|
||||
book_id: 7,
|
||||
};
|
||||
let sticks = Recipe {
|
||||
kind: Kind::CraftingShaped,
|
||||
width: 1,
|
||||
height: 2,
|
||||
slots: vec![vec![one(2)], vec![one(2)]],
|
||||
results: vec![Ingredient { count: 4, ..one(3) }],
|
||||
book_id: 8,
|
||||
..planks.clone()
|
||||
};
|
||||
let charcoal = Recipe {
|
||||
kind: Kind::Smelting,
|
||||
width: 0,
|
||||
height: 0,
|
||||
slots: vec![vec![one(1)]],
|
||||
results: vec![one(4)],
|
||||
cooking_time: 200,
|
||||
experience: 0.15,
|
||||
book_id: 9,
|
||||
};
|
||||
(items, vec![planks, sticks, charcoal])
|
||||
}
|
||||
|
||||
fn ids(out: &Out) -> Vec<&str> {
|
||||
match out {
|
||||
Out::Items(items) => items.iter().map(|s| s.item.as_str()).collect(),
|
||||
_ => panic!("{out:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_every_item_but_air_and_searches_names_ids_namespaces_and_tags() {
|
||||
let (items, all) = tables();
|
||||
let (mut v, out) = Viewer::new(items, all);
|
||||
assert_eq!(ids(&out).len(), 4);
|
||||
let search = |v: &mut Viewer, text: &str| {
|
||||
ids(&v.handle(Event::Search(text.into()), false, false, &[])[0]).len()
|
||||
};
|
||||
assert_eq!(search(&mut v, "oak"), 2);
|
||||
assert_eq!(search(&mut v, "#logs"), 1);
|
||||
assert_eq!(search(&mut v, "@mine stick"), 1);
|
||||
assert_eq!(search(&mut v, "@other"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_shows_recipes_and_a_right_click_uses_with_a_tab_per_station() {
|
||||
let (items, all) = tables();
|
||||
let (mut v, _) = Viewer::new(items, all);
|
||||
// Oak Log is listed first: it has no recipe, so nothing opens.
|
||||
assert!(
|
||||
v.handle(
|
||||
Event::Clicked {
|
||||
index: 0,
|
||||
right: false
|
||||
},
|
||||
false,
|
||||
false,
|
||||
&[]
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
let uses = v.handle(
|
||||
Event::Clicked {
|
||||
index: 0,
|
||||
right: true,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
&[8],
|
||||
);
|
||||
let Out::Recipes {
|
||||
title,
|
||||
tabs,
|
||||
tab,
|
||||
views,
|
||||
} = &uses[0]
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(title, "Uses: Oak Log");
|
||||
assert_eq!(
|
||||
tabs.iter().map(|t| t.item.as_str()).collect::<Vec<_>>(),
|
||||
[STATIONS[0], STATIONS[1]]
|
||||
);
|
||||
assert_eq!(*tab, 0);
|
||||
// Shapeless planks laid out in a 1 x 1 grid; the book doesn't hold it yet.
|
||||
assert_eq!(
|
||||
(views[0].layout, views[0].width, views[0].note.as_str()),
|
||||
(Layout::Grid, 1, "Shapeless")
|
||||
);
|
||||
assert_eq!(views[0].action, Some(false));
|
||||
let furnace = v.handle(Event::Tab(1), false, false, &[]);
|
||||
let Out::Recipes { views, .. } = &furnace[0] else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(
|
||||
(views[0].layout, views[0].note.as_str()),
|
||||
(Layout::Cooking, "10 s, 0.15 XP")
|
||||
);
|
||||
assert_eq!(
|
||||
v.handle(Event::Action(0), false, false, &[]),
|
||||
[Out::Place(9)]
|
||||
);
|
||||
// Back with one page shown: no page, the items again.
|
||||
let back = v.handle(Event::Back, false, false, &[]);
|
||||
assert!(matches!(&back[0], Out::Recipes { views, .. } if views.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keys_and_slot_clicks_open_pages_and_back_returns() {
|
||||
let (items, all) = tables();
|
||||
let (mut v, _) = Viewer::new(items, all);
|
||||
let stick = Stack {
|
||||
count: 64,
|
||||
..stack("minecraft:stick")
|
||||
};
|
||||
let out = v.handle(
|
||||
Event::Key {
|
||||
uses: false,
|
||||
stack: stick,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
&[8],
|
||||
);
|
||||
let Out::Recipes { title, views, .. } = &out[0] else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(title, "Recipes: Stick");
|
||||
assert_eq!(views[0].action, Some(true));
|
||||
let planks = v.handle(
|
||||
Event::SlotClicked {
|
||||
stack: stack("minecraft:oak_planks"),
|
||||
right: false,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
&[],
|
||||
);
|
||||
assert!(matches!(&planks[0], Out::Recipes { title, .. } if title == "Recipes: Oak Planks"));
|
||||
let back = v.handle(Event::Back, false, false, &[]);
|
||||
assert!(matches!(&back[0], Out::Recipes { title, .. } if title == "Recipes: Stick"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_creative_with_the_setting_a_click_gives() {
|
||||
let (items, all) = tables();
|
||||
let (mut v, _) = Viewer::new(items, all);
|
||||
let give = |v: &mut Viewer, right, give, creative| {
|
||||
v.handle(Event::Clicked { index: 2, right }, give, creative, &[])
|
||||
};
|
||||
assert_eq!(
|
||||
give(&mut v, false, true, true),
|
||||
[Out::Give {
|
||||
item: "minecraft:stick".into(),
|
||||
count: 1
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
give(&mut v, true, true, true),
|
||||
[Out::Give {
|
||||
item: "minecraft:stick".into(),
|
||||
count: STACK
|
||||
}]
|
||||
);
|
||||
// Off, or not in creative: the page as usual.
|
||||
assert!(matches!(
|
||||
&give(&mut v, false, false, true)[0],
|
||||
Out::Recipes { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
&give(&mut v, false, true, false)[0],
|
||||
Out::Recipes { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_setting_reads_the_webassembly_mods_value() {
|
||||
assert!(gives(Some("true".into())));
|
||||
assert!(!gives(Some("false".into())));
|
||||
assert!(!gives(None));
|
||||
assert_eq!(give_label(true), "Click gives items (creative): ON");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// The mod's settings page: the WebAssembly mod's one button.
|
||||
fn official_recipes_page(
|
||||
game: &mut crate::game::Game<'_>,
|
||||
mod_id: &str,
|
||||
buttons: &mut Vec<String>,
|
||||
) {
|
||||
use crate::game::official_recipes as m;
|
||||
if mod_id != m::ID {
|
||||
return;
|
||||
}
|
||||
buttons.push(m::give_label(m::gives(game.native_setting(m::ID, "give"))));
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Recipes in a running modded game: the WebAssembly mod's setting is read and written as it was, the panel
|
||||
beside a crafting table's screen shows items (container screens show it; the creative inventory doesn't), a
|
||||
click on one gives it in creative mode, and with the setting off the same click opens its recipe page."""
|
||||
import json
|
||||
import time
|
||||
from game import Game, difference
|
||||
|
||||
DATA = "horizon-mods/.horizon-mod-data/recipes"
|
||||
# A player's setting from the WebAssembly mod: "Click gives items" on.
|
||||
FILES = {f"{DATA}/give": "true"}
|
||||
SLOT = 18
|
||||
|
||||
|
||||
def first_item(g):
|
||||
"""The panel's first item (Stone, the first item of the registry): its centre in GUI pixels, and the panel's
|
||||
part of the screen."""
|
||||
ui = g.run("ui")
|
||||
width = ui["gui_size"][0]
|
||||
stone = next(w for w in ui["widgets"] if w.get("label") == "Stone" and w.get("kind") == "button")
|
||||
x, y, w, h = stone["bounds"]
|
||||
return x + w / 2, y + h / 2, ((x - 2 * SLOT) / width, 0.0, 1.0, 1.0)
|
||||
|
||||
|
||||
def table(g):
|
||||
"""Opens the crafting table placed beside the player."""
|
||||
x, y, z = (int(c // 1) for c in g.run("status")["player"]["position"])
|
||||
if not getattr(g, "table", None):
|
||||
g.run("command", f"setblock {x + 2} {y} {z} minecraft:crafting_table")
|
||||
g.table = (x + 2, y, z)
|
||||
time.sleep(1)
|
||||
g.run("interact", *map(str, g.table))
|
||||
time.sleep(1)
|
||||
assert g.run("screen").get("screen") not in ("playing", "creative"), g.run("screen")
|
||||
|
||||
|
||||
def run(ctl, resources, out):
|
||||
with Game(ctl, resources, out, FILES) as g:
|
||||
g.run("options", "mods")
|
||||
assert "Recipes 2.0.0: ON" in g.labels(), g.labels()
|
||||
g.run("click", g.ids()["Settings"])
|
||||
page = g.labels()
|
||||
assert "Click gives items (creative): ON" in page, page
|
||||
g.play()
|
||||
g.run("command", "clear @s")
|
||||
table(g)
|
||||
x, y, panel = first_item(g)
|
||||
shown = g.shot("1-panel")
|
||||
g.run("click-at", str(x), str(y))
|
||||
time.sleep(1)
|
||||
given = g.run("inventory").get("counts", {})
|
||||
assert given, "a click on the panel's first item gave nothing"
|
||||
g.play()
|
||||
g.run("options", "mods")
|
||||
g.run("click", g.ids()["Settings"])
|
||||
g.run("click", g.ids()["Click gives items (creative): ON"])
|
||||
stored = (g.home / DATA / "give").read_text()
|
||||
g.play()
|
||||
table(g)
|
||||
before = g.shot("2-items")
|
||||
g.run("click-at", str(x), str(y))
|
||||
time.sleep(1)
|
||||
after = g.shot("3-recipe-page")
|
||||
results = {
|
||||
"given": given,
|
||||
"give_file_after_click": stored,
|
||||
"diff_panel": round(difference(before, after, panel), 1),
|
||||
"diff_panel_first_shot": round(difference(shown, before, panel), 1),
|
||||
}
|
||||
assert stored == "false", results
|
||||
assert results["diff_panel"] > 4, results
|
||||
print(json.dumps({"proof": "recipes", "ok": True, **results}, indent=2))
|
||||
@@ -8,7 +8,8 @@
|
||||
python3 native/tools/native.py proof --resources PATH zoom # build, then drive the game with horizon-ctl
|
||||
--horizon REV build on another Horizon commit (`latest` = the newest Nightly) instead of the mods'
|
||||
compatibility.json: they use only the native hook points, so they build on any Horizon whose
|
||||
NATIVE_HOOKS_API is at least the one they name, as the launcher does.
|
||||
NATIVE_HOOKS_API is at least the one they name, as the launcher does. Without it, mods pinned
|
||||
to different commits build on the commit of those that need the most hook points.
|
||||
`all` names every mod in native/.
|
||||
|
||||
Horizon's source comes from GitHub's archive of the commit, never a local checkout; horizon-mixins is built once at
|
||||
@@ -206,7 +207,10 @@ def main():
|
||||
package(m)
|
||||
return
|
||||
if args.horizon is None:
|
||||
revs = {m.compat["horizon_revision"] for m in mods}
|
||||
# Mods on the hook points build on any Horizon with the hook points they need, as the launcher builds
|
||||
# them: the commit of the mods that need the most has them all (stage checks each mod's need).
|
||||
newest = max(m.compat["hooks"] for m in mods)
|
||||
revs = {m.compat["horizon_revision"] for m in mods if m.compat["hooks"] == newest}
|
||||
if len(revs) != 1:
|
||||
sys.exit(f"the mods were tested on different Horizons {sorted(revs)}: pass --horizon")
|
||||
revision = revs.pop()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"game": "26.3",
|
||||
"hooks": 1,
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_revision": "1149c92cb8c1fde5136197b6647a6eb220401a60",
|
||||
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
|
||||
}
|
||||
@@ -6,7 +6,7 @@ priority = 100
|
||||
# The commit it was tested on. It uses only the native hook points (hooks 1 in
|
||||
# compatibility.json): the launcher builds it on any Horizon that has them.
|
||||
[compatibility]
|
||||
horizon_revision = "1149c92cb8c1fde5136197b6647a6eb220401a60"
|
||||
horizon_revision = "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
@@ -31,6 +31,7 @@ kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_wheel"
|
||||
helper = "wheel.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&crate::game::Game<'_>", value = "&*self" },
|
||||
{ name = "steps", ty = "f32", value = "steps" },
|
||||
{ name = "claimed", ty = "&mut bool", value = "&mut *claimed" },
|
||||
]
|
||||
@@ -41,6 +42,7 @@ kind = "HEAD"
|
||||
target = "horizon_client::game::native_hooks::Game<'_>::native_zoom"
|
||||
helper = "zoom.rs"
|
||||
bindings = [
|
||||
{ name = "game", ty = "&crate::game::Game<'_>", value = "self" },
|
||||
{ name = "zoom", ty = "&mut Option<crate::game::native_hooks::NativeZoom>", value = "&mut *zoom" },
|
||||
]
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// WebAssembly Zoom kept it.
|
||||
fn zoom_key(game: &mut crate::game::Game<'_>, key: &str, pressed: bool) {
|
||||
use crate::game::official_zoom as zoom;
|
||||
// Turned off in Options > Mods: nothing until the next build leaves it out.
|
||||
if !game.native_on(zoom::ID) {
|
||||
return;
|
||||
}
|
||||
let bound = game.native_mod_key(zoom::ID, zoom::KEY, zoom::DEFAULT_KEY);
|
||||
if key != bound {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// While the key is held, the wheel zooms instead of moving the hotbar.
|
||||
fn zoom_wheel(steps: f32, claimed: &mut bool) {
|
||||
if crate::game::official_zoom::wheel(steps) {
|
||||
fn zoom_wheel(game: &crate::game::Game<'_>, steps: f32, claimed: &mut bool) {
|
||||
use crate::game::official_zoom as zoom;
|
||||
if game.native_on(zoom::ID) && zoom::wheel(steps) {
|
||||
*claimed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// The zoom in effect, unless another mod's came first.
|
||||
fn zoom_view(zoom: &mut Option<crate::game::native_hooks::NativeZoom>) {
|
||||
fn zoom_view(game: &crate::game::Game<'_>, zoom: &mut Option<crate::game::native_hooks::NativeZoom>) {
|
||||
if zoom.is_none()
|
||||
&& game.native_on(crate::game::official_zoom::ID)
|
||||
&& let Some((factor, smooth, turn)) = crate::game::official_zoom::view()
|
||||
{
|
||||
*zoom = Some(crate::game::native_hooks::NativeZoom {
|
||||
|
||||
Reference in new issue
Block a user