Compare commits

...
Author SHA1 Message Date
82e29462a9 SMP: the sidebar no longer lists the richest players (#148)
The sidebar keeps the player's own block (name, balance, playtime, online)
and the address. /baltop still ranks the richest. board.top is ignored.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Ludovic Marie <[email protected]>
2026-11-03 22:35:19 +01:00
725ff7a410 Update chatheads 0.1.1 (#151)
* Update chatheads 0.1.1

Submitted by @Domanffe through the form.

Submission: m9iIfQyNTA6ee66EpcsIdlIFDTgx3mIakQro02chGVU
Co-authored-by: Domanffe <[email protected]>

* chatheads: fix the doubled icon URL and restore the name ChatHeads

The submission form pasted the icon URL twice and lowercased the name; the
in-game name is still ChatHeads.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Domanffe <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 21:16:30 +01:00
Nuggetandadasjusk 64b5237f7c Add killc, stuck and rmotd native mods (#125)
Co-authored-by: adasjusk <[email protected]>
2026-11-03 21:16:24 +01:00
Ayano KaitoandGPT-6.1 Sol 7d17c4d63a Add Right-Click Crops 1.0.0 registry submission (#124)
Add the author's approved project page, icon and verified Minisign signature. Request maintainer staging because the web form rejects the submission origin.

Co-authored-by: GPT-6.1 Sol <[email protected]>
2026-11-03 21:16:18 +01:00
Matteo FranguelliandClaude Opus 5.5 aab9aa4c22 Add customhud (Beautiful Day Counter), a native mod (#115)
* Add customhud (Beautiful Day Counter), a native mod

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* customhud: list version 1.6.0 explicitly instead of repo

Every version of a native mod is reviewed, so the entry names its file
and SHA-256 rather than following the repository's releases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 21:16:11 +01:00
tariksabanuzandClaude Sonnet 5.5 6d9a7492e2 PhysicsModHorizon (#191)
* Add files via upload

* Delete mods/mod_menu-0.3.0.hnmod

* Add Horizon Physics 0.12.1 (native mod)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PaZj999vuofU9gSEtyNeUq

* Horizon Physics 0.13.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PaZj999vuofU9gSEtyNeUq

* Horizon Physics 0.14.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PaZj999vuofU9gSEtyNeUq

* horizon_physics 0.15.0

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PaZj999vuofU9gSEtyNeUq

* horizon_physics 0.15.1

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PaZj999vuofU9gSEtyNeUq

---------

Co-authored-by: Claude <[email protected]>
2026-11-03 20:04:20 +01:00
expensiverest 96c7c46e41 Radiant shader pack (#184) 2026-11-03 19:53:27 +01:00
AkirabaneandClaude Sonnet 5.5 09f69374d9 smp: /spawn lands on a safe spot within spawn_radius of the spawn point (#190)
/spawn teleported to one fixed point, which on the anarchy server can sit in
a lava cast. After the tp it now runs vanilla's spreadplayers around the
spawn point with places.spawn_radius (default 250, like the live server's
respawn_radius), which picks a random column that is not fluid, fire or
magma and has solid ground, retrying until one is found. Warps, homes, /back
and /tpa are unchanged. Radius 0 keeps the exact point.

Refs #144

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
2026-11-03 19:41:23 +01:00
Ludovic MarieandDomanffe 4e8a65e584 Update rightharvest 0.1.1 (#152)
Submitted by @Domanffe through the form.

Submission: raf1Of2yOgkT_BRuLqW580FVXVBZqItOyqTK-1tIrj4

Co-authored-by: Domanffe <[email protected]>
2026-11-03 18:37:21 +01:00
Ludovic MarieandDomanffe bba4bda2d7 Update blockscope 0.3.1 (#150)
Submitted by @Domanffe through the form.

Submission: yQxeJFeA1pxjYCJZusO7QY83yMPaOXOZ1mKmjvzqfiU

Co-authored-by: Domanffe <[email protected]>
2026-11-03 18:37:16 +01:00
Ludovic Marieandkeksi2k 6263473f81 Add horizonmatica 0.1.1 (#145)
Submitted by @keksi2k through the form.

Submission: DXHbuwRsHNMIModbM_217IiUgMiBXsORLzIecTtl5TI

Co-authored-by: keksi2k <[email protected]>
2026-11-03 18:37:10 +01:00
Ludovic MarieandVanishingTacos 1fc4bd47d5 Add inventory_wizard 2.3.3 (#120)
Submitted by @VanishingTacos through the form.

Submission: MV28vLDz8HAq_j0ApAE1yIC4BXlLuoDRF8RWnRBpWkg

Co-authored-by: VanishingTacos <[email protected]>
2026-11-03 18:37:04 +01:00
Ludovic MarieandLauch2 7558c0945c Add waymap 0.1.4 (#179)
Submitted by @Lauch2 through the form.

Submission: eLte5hlJxy2U5VIR8-lJIqN2qCkgJ-dqT4i3o95uIVY

Co-authored-by: Lauch2 <[email protected]>
2026-11-03 18:34:43 +01:00
Ludovic MarieandNirokx 467b17bf38 Add voicechat 1.0.0 (#169)
Submitted by @Nirokx through the form.

Submission: AG7Q9X_ycyCwuouRding4ld-wXJjIAaCwGlv0aXEloc

Co-authored-by: Nirokx <[email protected]>
2026-11-03 18:34:37 +01:00
Ludovic MarieandNirokx 3d113f473a Add waypoints 1.0.0 (#168)
Submitted by @Nirokx through the form.

Submission: Ad0rlAatq4jNcgAxUy5vaU21EsMFePIXwBTE_ARctBM

Co-authored-by: Nirokx <[email protected]>
2026-11-03 18:34:32 +01:00
Ludovic MarieandNirokx 792fa822f4 Add fullbright_nofog 1.0.0 (#167)
Submitted by @Nirokx through the form.

Submission: slTmWEe55jWWepzv9ESpVWpyu4VvTiF0Jcl-9vNxfoo

Co-authored-by: Nirokx <[email protected]>
2026-11-03 18:34:26 +01:00
Ludovic Marieandsoufied 7b65e9b10f Update appleskin 0.1.2 (#163)
Submitted by @soufied through the form.

Submission: 3Qw5P2ElK53IqvJ2sEHjEZ-MUba5C0tz8aAXwIK23OA

Co-authored-by: soufied <[email protected]>
2026-11-03 18:34:21 +01:00
Ludovic Marieandumbx-dot cd1f1c3970 Add flair 1.0.0 (#199)
Submitted by @umbx-dot through the form.

Submission: XpkshdIFDfLtP-GXJPDX1VGRlBjjT8geDr-JKyATzvk

Co-authored-by: umbx-dot <[email protected]>
2026-11-03 18:32:11 +01:00
Ludovic Marieandrepositorykreml1n be266234cb Update basic_hud 0.4.1 (#198)
Submitted by @repositorykreml1n through the form.

Submission: 9rPRCH2NULh3Iy5kRA1ctwN9shoKNDOc0jW7uTbaeNo

Co-authored-by: repositorykreml1n <[email protected]>
2026-11-03 18:32:06 +01:00
Ludovic MarieandLutziano888 cf1f608745 Add accurate_block_placement 1.0.0 (#196)
Submitted by @Lutziano888 through the form.

Submission: o1TJGn5bAsBJA0ZlAhulN0DVgOliaVK0rD2jxCIXlR8

Co-authored-by: Lutziano888 <[email protected]>
2026-11-03 18:31:59 +01:00
Ludovic MarieandMayonesu0 f1f5aba33a Add chop 1.0.1 (#194)
Submitted by @Mayonesu0 through the form.

Submission: ABM9UnrYSccp3Lk313QYJcJI19WMsFz5VGL2n2tr1bQ

Co-authored-by: Mayonesu0 <[email protected]>
2026-11-03 18:31:54 +01:00
Ludovic Marieandtaxtii 0062a1e27a Add hitbox_plus 1.3.0 (#193)
Submitted by @taxtii through the form.

Submission: rkAkGw06AMsDZfPvNUJJT6YIwW6BWpiWu_teeKlJdJU

Co-authored-by: taxtii <[email protected]>
2026-11-03 18:31:49 +01:00
Ludovic Marieandtariksabanuz e79b92ff2c Add resourcify 0.1.0 (#189)
Submitted by @tariksabanuz through the form.

Submission: z-ohfvwLJTjWO_6m0RzyB-KvPLI-vNMmUkZ-vVnklR8

Co-authored-by: tariksabanuz <[email protected]>
2026-11-03 18:31:43 +01:00
Ludovic Marieandtariksabanuz ce14e90beb Add mod_menu 0.3.0 (#188)
Submitted by @tariksabanuz through the form.

Submission: YwrDR2k6vkOZjxp4ucVa-sBOHH2mUnBd45UuaIDYwys

Co-authored-by: tariksabanuz <[email protected]>
2026-11-03 18:31:38 +01:00
Ludovic MarieandJustKarkat 93b3f04390 Update crossversion 0.8.0 (#186)
Submitted by @JustKarkat through the form.

Submission: v_bcduqKfPvkdT3qbeHXZ9scM3YNOSvwoWEXL7agSes

Co-authored-by: JustKarkat <[email protected]>
2026-11-03 18:30:54 +01:00
Ludovic MarieandClaude Opus 5.5 bc40e7bafe Take crossversion 0.7.1 down (revoked.toml) (#185)
* Revert "Add crossversion 0.7.1 (#180)"

This reverts commit 7645804495: the listing
comes down while the mod's account handling is reviewed with its author.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* revoked: turn installed crossversion 0.7.1 copies off

Removing the entry stops new installs only; the revocation also turns
copies players already installed off, with the reason shown to them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Keep the crossversion listing; the revocation alone takes 0.7.1 down

The registry refuses removing a listing ("take its files down in revoked.toml instead").

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 05:04:42 +01:00
Ludovic MarieandClaude Opus 5.5 5628be3e6b voicechat-server: proximity voice chat on Horizon's own server (native server mod) (#173)
* voicechat-server: proximity voice chat on Horizon's own server

A native server mod (voicechat_server 1.0.0): the server half for voice chat
clients that speak Simple Voice Chat 2.6.x's protocol, such as azetech's
Complex Voice Chat. The handshake rides Horizon's mod messages on voicechat:
channel names; voices go over UDP through the server, sealed per player with
AES-128-GCM, to the players nearby in the same dimension or in the speaker's
group. A player without a voice client is never sent anything, and the UDP
port opens only when the first voice client asks.

The relay is a bundle crate (horizon-mixins 0.6.0): every native mod now pins
horizon-mixins d9c83d7, since native.py builds them with one, and package
includes a bundle's crates.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* voicechat-server: replay-proof re-authentication; packages carry LICENSE

- A secret belongs to the first address that authenticates with it: an
  Authenticate copied and sent from another address is refused however long
  the client was quiet (the 5 s move rule is gone). Exact replays are dropped
  too: the last 64 control packets' nonces per player, and a 64-wide window
  on voice packets' sequence numbers.
- A client the server stops hearing for 10 keep-alives gets a fresh secret
  over the game connection (Complex Voice Chat 2.1.0 reconnects on any
  voicechat:secret), keeping its group; the old key then opens nothing. So
  a NAT that rebinds recovers without a rejoin (2.1.0 never re-sends
  Authenticate after its ack, so the old move rule never helped it).
- Secret requests: one answer a second a player (more requests meanwhile
  get one answer), the snapshot replaces what still waited. Group and state
  changes 2 a second (burst 10). At most 512 messages wait per player.
- Client pings aren't echoed (the server never pings; SVC's server doesn't
  echo either), and the relay thread never spins on a socket error.
- The glue lists the players on a message only when the sender is new, not
  on every voice chat message.
- native.py package puts native/<mod>/LICENSE into the .hnmod as
  mixins/<id>/LICENSE (Horizon's bundle reader accepts LICENSE* there); the
  CI check requires it. Every native mod's package now carries its licence.

Tests: a_replayed_handshake_after_a_silence_moves_nothing,
a_client_the_server_stops_hearing_gets_a_fresh_secret,
repeated_secret_requests_and_group_changes_stay_bounded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 04:16:31 +01:00
Ludovic MarieandClaude Opus 5.5 58fbad4a85 SMP 0.31.0: the auction house lists whole stacks (#183)
rdbt's ask: /ah refused anything that wasn't a plain item. A listing now
carries the whole stack (enchantments, name, damage, lore, a shulker box's
or a bundle's contents, every component) as the game writes it in player
files (ItemStack.CODEC NBT, hex), through listing, purchase, auction win,
mail and returns, saved in economy.json in the same step as the money.
Given back with native_give_stack; what doesn't fit waits in the mail.

Taken only when it reads back equal; not market charts, nor stacks over
32 KiB of data. Creative players are still refused. Listings and mail from
0.30 (no `stack`) load unchanged and give plain items as before.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 04:04:06 +01:00
Ludovic MarieandJustKarkat 7645804495 Add crossversion 0.7.1 (#180)
Submitted by @JustKarkat through the form.

Submission: eL0F1_shzScYc6vd_ujpkodYloPjMgUo2cfokvjAw28

Co-authored-by: JustKarkat <[email protected]>
2026-11-03 02:51:04 +01:00
levzzzandClaude Opus 5.5 e5d840dabf Add Motion Blur (native mod) (#171)
* Add Motion Blur (native mod, levzzz5154/horizon-motion-blur)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* motion_blur: the registry's current entry format

source and a [[versions]] entry instead of repo; the file is in the
staging release as <sha256>.hnmod, as the submission form puts it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-03 00:58:53 +05:00
Ludovic MarieandClaude Opus 5.5 b22a9c42a3 SMP 0.30.1: the claim-free zone covers every dimension (#162)
rdbt: the zone around 0 0 should apply in the Nether and the End too.
Claims there are refused and dropped at start in every dimension.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-02 17:01:15 +01:00
Ludovic MarieandClaude Opus 5.5 6ab145fce5 SMP 0.30.0: a claim-free zone around 0 0 (claims.free_radius) (#159)
Operators asked for the land near spawn to be free for all: no one may
claim a chunk that touches the zone, and claims already there are dropped
at start, each logged. Off by default (0).

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-02 16:09:20 +01:00
c240394cde smp: simplify the economy: no taxes, fees, Treasury, bank, jobs or futures (#153)
* smp: simplify the economy, drop the tax code

No more VAT on auction sales, weekly land and wealth taxes, /taxes and
/eco taxes, tax on market trades or teleport fees. The Treasury stays
(fees and house edges in, operators' grants out) with /treasury, now in
its own small module. Old taxes/tax_percent/fees config keys are ignored
and owed land tax is forgiven.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* smp: remove the Treasury, the bank, jobs and futures, and every fee

Operators make money with /eco give <player> <amount> <reason> (everyone
reads it) and take it with /eco take. The auction listing fee and the
coinflip/lottery fee are gone. The casino's pot gets its float once, made
for it, then keeps the house edge.

A world from before this gets its bank savings and open futures margins
back at the first start; the Treasury, insurance fund, contracts and land
tax owed are dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* smp: keep the casino's float mark in economy.json

The "pot got its float" flag lived in casino.json while the money went to
economy.json, so a lost or unreadable casino.json, or a crash between the
two saves, made up to 20,000 again on the next start. The mark now sits in
the economy's ext, written in the same file as the pot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Ludovic Marie <[email protected]>
2026-11-02 15:02:43 +01:00
Ludovic MarieandClaude Opus 5.5 d57056b937 SMP 0.29.1: an operator may open their own ender chest with /endersee (#156)
/invsee on yourself stays refused (it would move stacks into the same
inventory); your own ender chest is a second container, so /endersee works.
Asked by bunchy7s (operator) on Discord.

Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-02 13:36:10 +01:00
Ludovic Marie 245ab9b4f6 Merge pull request #143 from horizon-rust/feat/smp-one-price
SMP 0.29.0: one market price per item, to buy and to sell
2026-11-02 00:43:42 +01:00
Ludovic MarieandClaude Opus 5.5 61e99d5df7 SMP 0.29.0: carry the old price history and futures entries over
A book from before 0.29 quotes its history in the old mid of buy and sell
(diamond 250); the one price is 200. Scaled once at load (the book now
records its bases), so charts, 1 h/24 h moves, the news and the futures'
TWAP see no cliff; open futures positions get the same ratio on their
entry, so none gains or loses by it. Each minute's band opens at the
loaded price rather than at the base.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 00:28:31 +01:00
Ludovic MarieandClaude Opus 5.5 59f2388a0f SMP 0.29.0: one market price per item, to buy and to sell
Ludovic: the market's buy and sell prices didn't make sense (diamond bought
for 400 and sold for 100, two numbers everywhere). Each item now has one
price, the geometric mean of the old pair (diamond 200, iron 15, wheat 2),
shown in /worth, /market, /shop, the chart maps and market.json (`price`;
`buy`/`sell` repeat it on the sides the market trades).

- A trade of N is priced along the curve it moves (each unit moves the log
  price by 1%/depth): buying N and selling them back returns exactly what
  was paid less the tax, for 1, 64 or past the band. Rounding goes against
  the trader (up when buying, down when selling).
- Prices move as trades happen; all trades together move an item at most
  5% a minute: past that a purchase is cut short and a sale keeps the
  rest (flat pricing at the edge would let slow buying plus a flat dump
  earn). At the 0.6/1.6 factor bounds the rest goes at the bound.
- Items linked by recipes share one moving price, a block moving it as its
  9 ingots do; the vanilla-recipe test checks every item at the worst
  drift too, and finds a block sold for its ingots once unlinked.
- The tax is its own receipt line ("Tax 2%: -12 coins. You get 583
  coins."). Daily sell limits unchanged.
- A config with two prices gets one at load, logged ("one price now").

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 00:20:33 +01:00
Ludovic Marie 5b0ae21c96 Merge pull request #141 from horizon-rust/fix/smp-casino-win-cap
SMP 0.28.0: casino daily win cap (10,000 net a day)
2026-11-01 22:53:26 +01:00
Ludovic Marie 097ccd28b0 Merge pull request #135 from horizon-rust/feat/smp-futures
SMP 0.25.0: futures with leverage on items (/futures)
2026-11-01 22:53:01 +01:00
Ludovic MarieandClaude Opus 5.5 31d77513fe SMP 0.28.0: the casino's daily win cap (10,000 net a day, configurable)
A player whose net casino winnings today reach casino.daily_win_cap
(10,000, same as the loss cap) is refused new bets until 00:00 UTC, with
a message saying why. The round that crosses the cap is paid in full; a
blackjack double inside a hand already dealt is still allowed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 22:43:50 +01:00
Ludovic MarieandClaude Opus 5.5 5ba617927f SMP 0.27.0: futures in the game (/futures menu, minute marking)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 22:43:47 +01:00
Ludovic MarieandClaude Opus 5.5 7257e32237 SMP futures: the rules (perpetuals on market prices, house = insurance fund)
Not wired into the game yet: the glue waits for the market's mark_price/twap/volume API.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 22:43:46 +01:00
Ludovic Marie 18102bf315 Merge pull request #134 from horizon-rust/feat/smp-dyn-market
smp 0.26.0: a moving market, /market, price charts on maps, pages pushed to Kira
2026-11-01 22:43:09 +01:00
Ludovic MarieandClaude Opus 5.5 7ec0a0516d smp proof: the selling limit counts Steve's /pay (#139), Moves may read 0.0% on a minute-old history
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 22:36:10 +01:00
Ludovic MarieandClaude Opus 5.5 c3839da0ef smp 0.26.0: a moving market, /market, price charts on maps, pages pushed to Kira
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 21:02:28 +01:00
Ludovic Marie 2f677e9337 Merge pull request #129 from horizon-rust/feat/smp-invsee
smp 0.25.0: /invsee and /endersee for operators
2026-11-01 19:52:00 +01:00
Ludovic MarieandClaude Opus 5.5 8295b4c1ef smp 0.25.0: /invsee and /endersee for operators
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 19:42:27 +01:00
Ludovic Marie cacc3360e0 Merge pull request #139 from horizon-rust/fix/smp-pay-cap
SMP 0.24.0: /pay received counts toward the daily selling limit
2026-11-01 19:36:55 +01:00
Ludovic MarieandClaude Opus 5.5 964325b819 SMP 0.24.0: /pay received counts toward the daily selling limit
Selling up to the cap on alts and paying it all to a main bypassed the daily sell
limit (reported by pointofpressure). A /pay now checks the receiver's remaining
daily_sell_coins and is refused past it; a payment made counts toward it. Trades
and auction sales are unaffected.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 19:30:11 +01:00
Ludovic Marie 9ce6a3f9a0 Merge pull request #137 from horizon-rust/feat/smp-casino
SMP 0.23.0: the casino (roulette, slots, crash, blackjack)
2026-11-01 19:07:18 +01:00
Ludovic MarieandClaude Opus 5.5 693205032b SMP 0.23.0: the casino (roulette, slots, crash, blackjack), paid from its own pot
/casino opens a lobby menu; each game is a chest menu a stock client plays,
animated at most 5 pictures a second per open menu and only while a round
moves, with sounds, titles, the action bar, a boss bar for crash, fireworks
and a line for everyone on a jackpot.

- European roulette (2.70% edge), slots with a published paytable (94.2%
  paid back), one shared crash round (96%), blackjack S17 3:2 (~1% edge).
- Draws by the server (ChaCha20 keyed from the system) before the
  animation. Bet limits, a daily loss cap a player, a cooldown.
- Bets are held, then settled once through settle_pool into the casino's
  pot (capped, never minted); hourly the pot above its float goes to the
  Treasury (casino_profit), below it the Treasury tops it up (casino_float).
- Closing the menu or leaving never loses a round.
- casino.json: the day's ledger, wins, totals. proof_casino.py plays every
  game on a real server with two clients.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:59:50 +01:00
Ludovic Marie e8b243b60d Merge pull request #126 from horizon-rust/feat/smp-nick-ops
smp 0.22.0: nicknames for operators only
2026-11-01 18:32:20 +01:00
Ludovic MarieandClaude Opus 5.5 409cac606e smp 0.22.0: nicknames for operators only
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:24:39 +01:00
Ludovic Marie d4463c5a82 Merge pull request #133 from horizon-rust/feat/smp-jobs
SMP 0.21.0: daily contracts (/jobs), paid from the Treasury
2026-11-01 18:22:51 +01:00
Ludovic Marie e001cceadd Merge pull request #132 from horizon-rust/feat/smp-report
SMP 0.20.0: /eco stats, the daily economy report, web/economy.json
2026-11-01 18:22:47 +01:00
Ludovic Marie 2b62bd35d9 Merge pull request #131 from horizon-rust/feat/smp-bank
SMP 0.19.0: the bank (/bank), savings paid by the Treasury
2026-11-01 18:20:31 +01:00
Ludovic MarieandClaude Opus 5.5 219c9302d9 SMP 0.21.0: daily contracts (/jobs), paid from the Treasury
Four contracts a day from farm, mine and mob produce; a delivery of 64 plain items
pays the market's sell price +30%, never over 90% of its ask (rechecked at
delivery), from the Treasury. 8 deliveries a contract, 2 a player a day, at most
10% of the Treasury a day, survival/adventure only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:18:13 +01:00
Ludovic Marie e9f25bb1d7 Merge pull request #130 from horizon-rust/feat/smp-sinks
SMP 0.18.0: travel fees to the Treasury, a daily reward for playing
2026-11-01 18:14:15 +01:00
Ludovic MarieandClaude Opus 5.5 bb97027039 SMP 0.20.0: /eco stats, the daily economy report, web/economy.json
/eco stats: the supply and where it sits, what the server made and destroyed
today (new per-kind flows in economy.json), the day's taxes, Gini and top-10
share. At 00:00 UTC everyone reads the day before; 60 daily snapshots kept in
ext.report. web/economy.json once a minute, written off the tick thread, with
every Treasury movement and the tax code, for /hub.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:13:49 +01:00
Ludovic MarieandClaude Opus 5.5 27bfc9da13 SMP 0.19.0: the bank (/bank), savings paid by the Treasury
Deposits wait in the `bank` pool; interest 0.1% a day on up to 10,000 coins,
paid at 00:00 UTC from the Treasury on the day's lowest amount (no
deposit-before-midnight trick), at most 5% of the Treasury a night, pro rata.
At most 25,000 saved. The wealth tax counts savings. No loans.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:08:26 +01:00
Ludovic MarieandClaude Opus 5.5 fa61a38111 SMP 0.18.0: travel fees to the Treasury, a daily reward for playing
Teleports cost a fee (home 5, warp 10, back 10, tpa 10, spawn free), checked when
the command runs and paid to the Treasury when the teleport happens; one called off
costs nothing and keeps /back and the cooldown. The daily reward waits for 10
minutes of play that day (moving or commands), so alts that only log in get
nothing. Proof: fees and delay off to keep its sums; config_keys gets `auction`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 18:04:22 +01:00
Ludovic Marie 169ac59750 Merge pull request #128 from horizon-rust/feat/smp-auction
SMP 0.17.0: the auction house (/ah)
2026-11-01 17:59:05 +01:00
Ludovic MarieandClaude Opus 5.5 7c850a3056 SMP 0.17.0: the auction house (/ah)
Players sell to players: fixed-price listings or timed auctions, browsed
in a chest menu by category (or a search), cheapest a unit first.

- Listing fee 1% (min 1 coin) and 5% VAT on each sale go to the Treasury.
- Listed items leave the inventory at once; unsold ones, auction wins and
  what didn't fit come back by mail (/ah claim, or the menu's mail page).
- A purchase is one step under the SMP's lock: the listing leaves first,
  so of two buyers at once the second finds it gone.
- Bids are held in the `auction` pool; the outbid get theirs back at once;
  a bid in the last 60 s moves the end back to 60 s; offline sellers are
  paid and get a note when they're back.
- Listings, bids, mail and notes are written in economy.json with the
  money (ext.auction): a crash can't split items from coins.
- Plain items only until native_give_stack is stamped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 17:51:31 +01:00
Ludovic Marie e3ee0a7c45 Merge pull request #127 from horizon-rust/feat/smp-treasury
SMP 0.16.0: a state Treasury, nobody makes money, the tax code
2026-11-01 17:51:16 +01:00
Ludovic MarieandClaude Opus 5.5 3f115b7e06 SMP: the pools other parts use aren't dead code in the game build
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 17:47:43 +01:00
Ludovic MarieandClaude Opus 5.5 a864f877ec SMP 0.16.0: a state Treasury, nobody makes money, the tax code
Every tax, fee and house edge now goes into a Treasury pool kept in
economy.json with the balances; operators pay only from it, with a reason
everyone sees (/treasury pay, logged as `grant`). /eco give, /eco set and
/eco give * are gone: nobody can make money, operators included. /eco take
moves a cheat's gains into the Treasury.

- smp_economy: system pools (`pools`: treasury, insurance, casino...),
  to_pool / from_pool / pool_move / settle_pool, each one audit line; the
  Treasury's income and spending by kind (today, all time) and its last 50
  movements; `ext` for other parts' state written in the same step as the
  money. award()'s fee goes to the Treasury.
- smp_tax: the tax code (VAT 5% for the auction house, land tax 5 coins a
  week per chunk past 4, wealth tax 1/2/3% above 50k/250k/1M a week),
  collected on Monday 00:00 UTC (an O(1) check each second), unpaid land tax
  owed and blocking new claims; /treasury, /treasury log, /eco taxes,
  /eco taxes pay.
- proof: funds players from a seeded Treasury instead of /eco give, and
  checks /treasury, /treasury log, /eco taxes and the refusals.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 17:39:49 +01:00
Ludovic Marie 41874a0e0c Merge pull request #121 from horizon-rust/submit/astral_cruiser-0.1.0
Add astral_cruiser 0.1.0
2026-11-01 08:32:48 +01:00
Ludovic Marie 48ed3ea061 Merge pull request #122 from horizon-rust/submit/basic_hud-0.1.0
Add basic_hud 0.1.0
2026-11-01 08:32:11 +01:00
Ludovic MarieandClaude Opus 5.5 8e5a70b52b astral_cruiser: say what blasting breaks and where the ship is saved
Review fix by Ludovic's AI agent (Claude): the nose guns and beams blow up
blocks when blasting is on (off by default), and the ship is kept in the
world's folder (astral_cruiser/ship.json).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 08:31:51 +01:00
Ludovic Marieandrepositorykreml1n d31c2d7992 Add basic_hud 0.1.0
Submitted by @repositorykreml1n through the form.

Submission: aXw6Cm5ItBnfJZ5XYEO0w5xfcjfLs6I5Vvv5h9vqr6M
Co-authored-by: repositorykreml1n <[email protected]>
2026-11-01 07:15:41 +01:00
Ludovic Marieandrepositorykreml1n 9f691e7e83 Add astral_cruiser 0.1.0
Submitted by @repositorykreml1n through the form.

Submission: CCBrVRl_Te6V2LQUa8oSqf6mTpFOgWk62yydwskMFw4
Co-authored-by: repositorykreml1n <[email protected]>
2026-11-01 05:43:12 +01:00
Ludovic Marie c5d504a734 Merge pull request #119 from horizon-rust/docs/native-mods-build-path
docs: point native-mod build instructions at the Mods page
2026-11-01 02:54:07 +01:00
Ludovic MarieandClaude Sonnet 5.5 6c2a827c34 docs: point native-mod build instructions at the Mods page
"Settings → Game → Native mods (experimental)" no longer exists. The launcher builds
native mods from the Mods page (Build on the native build row); the game lists and
toggles them under Options → Mods. Prose only: no sha256, file or version changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-01 02:52:17 +01:00
Ludovic Marie 90f06bd473 Merge pull request #117 from horizon-rust/fix/smp-nick-abuse
SMP 0.15.0: nicknames that can't pass for someone else
2026-11-01 01:02:38 +01:00
Ludovic MarieandClaude Opus 5.5 3395149802 SMP 0.15.0: nicknames that can't pass for someone else
rdbt (Team moderator) reported /nick abuse on the official server. Nicknames now:
- show as ~name in chat, the Tab list and the sidebar;
- are compared case-insensitively, with _ dropped and look-alikes folded (0/o, 1/i/l, 3/e...);
- are refused when equal to, or containing, a known player's name (economy accounts, usercache.json), an
  operator's, a Team member's (nicks.protected) or another player's nickname, or when they read as a reserved
  word (server, console, admin, mod, owner, staff, op, horizon, kira, notch, mojang, herobrine...);
- need nicks.min_playtime_minutes (60) of play first, operators excepted; the chat filter and cooldown stay;
- that break these rules are taken off at start, each logged.

/pay, /tpa, /trade, /playtime, /tpaccept, /tpdeny and /eco take a nickname where they take a player's name;
/nick alone says how to take it off. The proof covers each rule end to end.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 00:58:13 +01:00
Ludovic Marie 3fc6964447 Merge pull request #116 from horizon-rust/feat/smp-scoreboard
SMP 0.14.0: the sidebar as an SMP scoreboard, the player's own block first
2026-10-31 23:50:36 +01:00
Ludovic MarieandClaude Opus 5.5 3749d679bd SMP 0.14.0: the sidebar as an SMP scoreboard, the player's own block first
Title "Horizon SMP": the player's name (their nickname if set), balance,
playtime and the players online; then the three richest; then the
server's address (board.footer, play.playhorizon.dev). The ranking is still
built once for everyone, on a balance change or every refresh_seconds; the
players' own lines are looked at on the same refresh_seconds and sent only
where they changed (and to newcomers). 0.13's untouched defaults (Richest
players, top 10) move to the new ones. Feedback from rdbt (Team moderator).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 23:46:25 +01:00
Ludovic Marie d933c333e6 Merge pull request #114 from horizon-rust/feat/smp-nick
SMP 0.13.0: nicknames (/nick, /realname)
2026-10-31 22:59:15 +01:00
Ludovic MarieandClaude Opus 5.5 c86a73887a SMP 0.13.0: nicknames (/nick, /realname)
/nick <name> and /nick off for players, /nick <player> <name|off> and
/realname <nick> for operators. The nickname shows in chat and the Tab list
(horizon API 62: native_set_nickname), the hover keeps the real name, and
the log, the anticheat and Discord keep the real one. Rules: 3-16 letters,
digits or _; never a known player's name, an operator's or another
player's nickname; through the chat filter (native_text_allowed); a
10-minute cooldown. Saved in world/horizon-smp/nicks.json.

Also: the redundant 'eco give * <amount>' literal is gone (the engine left
it out: '*' isn't a command word; <player:word> takes it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:55:57 +01:00
Ludovic Marie 83c4269800 Merge pull request #109 from horizon-rust/docs/reviewable-licences
Policy: list only source reviewers may read; every update is reviewed
2026-10-31 22:46:29 +01:00
Ludovic MarieandClaude Opus 5.5 89d90c3376 Policy: list only source reviewers may read; every update is reviewed
MOD-POLICY gets the licence rule reviewers already follow: a new project or
a new version needs source under a permissive licence (MIT, MIT-0, BSD,
0BSD, Apache-2.0, ISC, zlib, Unlicense, CC0) or a written grant to the
Horizon reviewers. classic_zoom, distant_horizons, fly_speed and serverify,
listed before, stay as they are until their next version.

The README no longer promises owner updates without a review: every version
of a native mod runs with the player's account permissions, so a reviewer
reads it first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:45:40 +01:00
Ludovic Marie 6b71454d1b Merge pull request #96 from horizon-rust/submit/freecam-0.1.0
Add freecam 0.1.0
2026-10-31 22:38:28 +01:00
Ludovic Marie 57f7902809 Merge pull request #94 from horizon-rust/submit/fair_freecam-1.2.0
Add fair_freecam 1.2.0
2026-10-31 22:38:22 +01:00
Ludovic Marie 7c2b9f3b80 Merge pull request #90 from horizon-rust/submit/freecam_plus-1.0.0
Add freecam_plus 1.0.0
2026-10-31 22:38:16 +01:00
Ludovic Marie 2a4307638f Merge pull request #83 from horizon-rust/submit/menu_panorama-panorama-1.0.0
Add menu_panorama panorama-1.0.0
2026-10-31 22:38:10 +01:00
Ludovic Marie 5108050b31 Merge pull request #100 from horizon-rust/submit/freelook-0.3.1
Add freelook 0.3.1
2026-10-31 22:37:32 +01:00
Ludovic Marie 706f3a6cb1 Merge pull request #95 from horizon-rust/submit/crosshair_indicator-1.0.0
Add crosshair_indicator 1.0.0
2026-10-31 22:37:24 +01:00
Ludovic Marie 6d05ae816f Merge pull request #89 from horizon-rust/submit/toggle_sprint-1.0.0
Add toggle_sprint 1.0.0
2026-10-31 22:37:16 +01:00
Ludovic Marie f5487d5b08 Merge pull request #88 from horizon-rust/submit/status_effect_hud-1.0.0
Add status_effect_hud 1.0.0
2026-10-31 22:37:09 +01:00
Ludovic Marie 4ede2e8aa7 Merge pull request #87 from horizon-rust/submit/light_overlay-1.0.0
Add light_overlay 1.0.0
2026-10-31 22:37:02 +01:00
Ludovic Marie 9e1a2b70a8 Merge pull request #85 from horizon-rust/submit/inventory_profiles-1.0.0
Add inventory_profiles 1.0.0
2026-10-31 22:36:56 +01:00
Ludovic Marie 969533780c Merge pull request #79 from horizon-rust/submit/fullbright_horizon-1.0.0
Add fullbright_horizon 1.0.0
2026-10-31 22:36:47 +01:00
Ludovic Marie 31e019dff8 Merge pull request #77 from horizon-rust/submit/armor_status-1.0.0
Add armor_status 1.0.0
2026-10-31 22:36:40 +01:00
Ludovic Marie cccfc99d37 Merge pull request #76 from horizon-rust/submit/appleskin_horizon-1.0.0
Add appleskin_horizon 1.0.0
2026-10-31 22:36:33 +01:00
Ludovic Marie 9c12738fb0 Merge pull request #71 from horizon-rust/submit/worldedit-1.2.1
Add worldedit 1.2.1
2026-10-31 22:36:28 +01:00
Ludovic Marie 6d9e3a2064 Merge pull request #59 from horizon-rust/docs/no-wasm-docs
Docs: drop the WebAssembly mod wording
2026-10-31 22:36:23 +01:00
Ludovic MarieandClaude Opus 5.5 9f7346e2e5 freecam: credit and link the original Freecam mod (MOD-POLICY, promise 2)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:35:17 +01:00
Ludovic Marie a04b258daf Merge remote-tracking branch 'origin/main' into fix-freecam 2026-10-31 22:35:16 +01:00
Ludovic MarieandClaude Opus 5.5 1b4ab4efe8 menu_panorama: clean the page, version 1.0.0, icon in the registry
The description started with the form's paste instructions; the version
was "panorama-1.0.0" (the pack's own version is 1.0.0); the launcher reads
icons from icons/<id>.png.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:35:10 +01:00
Ludovic Marie 0eee668de4 Merge remote-tracking branch 'origin/main' into fix-menu_panorama 2026-10-31 22:34:56 +01:00
Ludovic MarieandClaude Opus 5.5 a5fdef87fb freecam_plus: host the icon in the registry
The launcher reads icons from icons/<id>.png, not raw.githubusercontent.com.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:34:44 +01:00
Ludovic Marie 44fa384ad6 Merge remote-tracking branch 'origin/main' into fix-freecam_plus 2026-10-31 22:34:42 +01:00
Ludovic MarieandClaude Opus 5.5 f25ae007a7 fair_freecam: host the icon in the registry
The entry's icon URL pointed at the repository's old name; the launcher
reads icons from icons/<id>.png.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:34:36 +01:00
Ludovic Marie 181b9af7de Merge remote-tracking branch 'origin/main' into fix-fair_freecam 2026-10-31 22:34:35 +01:00
Ludovic Marie c6c309d01b Merge pull request #105 from horizon-rust/feat/smp-shop
SMP 0.12.0: /shop menu, daily selling limits, no profitable recipe loop, /money and /eco give *
2026-10-31 21:22:47 +01:00
Ludovic Marie a258ef03d9 Merge pull request #102 from horizon-rust/feat/smp-presence
SMP 0.11.0: first-join welcome, daily reward, AFK out of the sleep count, a command rate limit
2026-10-31 21:14:56 +01:00
Ludovic Marie acf5d8896a Merge pull request #101 from horizon-rust/feat/smp-polish
SMP 0.10.0: /back after a death or a teleport, /playtime, /claim show
2026-10-31 21:14:42 +01:00
Ludovic Marie 4dcb8fed29 Merge pull request #97 from horizon-rust/feat/smp-board
SMP 0.9.0: a sidebar of the richest players, /sidebar on|off, the Tab list's header and footer
2026-10-31 21:14:17 +01:00
Ludovic MarieandClaude Opus 5.5 f56752d74f SMP 0.12.0: /shop menu, daily selling limits, no profitable recipe loop, /money and /eco give *
- /shop: the market as a 6-row chest menu: categories (config.json `shop`), a click buys one, a shift-click 64,
  a click on a stack of yours sells it, "Sell all"; balance, today's limit and help in the bottom row. Through the
  market's own rules; items move by the console's clear/give, measured before and after.
- Market: buy always dearer than sell (else the sale closes), no recipe chain makes a sold item for less than it
  sells (smp_recipes, Bellman-Ford over the recipes; the tests walk every vanilla recipe when the data is there),
  daily limits per player (2,000 coins, 640 of each item, market.json), checked arithmetic, item ids validated.
- 56 default prices in 7 categories; a config still holding the 18 old defaults gets the new ones.
- /money give|take|set (= /eco), /eco give * <amount> for everyone on; recipients are told; all logged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 21:13:11 +01:00
Ludovic Marie 455a8b800f Merge branch 'feat/smp-presence' into feat/smp-shop 2026-10-31 20:47:27 +01:00
Ludovic Marie 238d9cd1f4 Merge branch 'feat/smp-polish' into feat/smp-presence 2026-10-31 20:47:18 +01:00
Ludovic Marie ddab3fa269 Merge branch 'feat/smp-board' into feat/smp-polish 2026-10-31 20:47:08 +01:00
Ludovic MarieandClaude Opus 5.5 a301fcc469 SMP: pinned to horizon main with #5390 (API 49)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 20:47:04 +01:00
Ludovic Marie c1ce388d41 WIP shop 2026-10-31 20:45:32 +01:00
Ludovic Marie 8bbf748e1f Merge pull request #72 from horizon-rust/feat/smp-grief
SMP: keep water, fire, pistons, explosions, buckets and blows out of claims
2026-10-31 20:17:02 +01:00
Ludovic Marie 50c23cb7d9 Merge pull request #69 from horizon-rust/feat/smp-claims
SMP: land claims (/claim, /trust)
2026-10-31 20:16:54 +01:00
Ludovic Marie 8d5c1ed7a3 Merge pull request #66 from horizon-rust/feat/smp-places
SMP: places (spawn, homes, warps, /tpa) and /rules
2026-10-31 20:16:44 +01:00
levzzz b6c1af58cd Merge pull request #103 from horizon-rust/registry/veinminer-repo
veinminer 0.3.2 (Nightly 2026-10-31)
2026-11-01 00:06:14 +05:00
Ludovic MarieandClaude Opus 5.5 3cf1b6aa8f SMP 0.11.0: first-join welcome, daily reward, AFK out of the sleep count, a command rate limit
A new presence module, looked at once a second: a player's first join is
announced and they read the welcome and the rules; each player gets a
daily reward (logged as daily_reward); a player who doesn't move, turn
or run a command for afk_minutes, or types /afk, leaves the sleep count
(native_sleep_ignored) until they move; and a player runs at most
spam_commands SMP commands in spam_seconds. All in config.json's
presence section.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 20:05:56 +01:00
Ludovic MarieandClaude Opus 5.5 a11db16db3 SMP 0.10.0: /back after a death or a teleport, /playtime, /claim show
/back returns to where the player died or where their last teleport took
them from, with the same warmup and cooldown. /playtime reads the time
played that places already counts. /claim show draws the chunk's border
with dust particles for 10 s, to that player only, coloured by whose
land it is.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 20:05:42 +01:00
Ludovic MarieandClaude Opus 5.5 dca26bf044 SMP: a sidebar of the richest players, /sidebar on|off, and the Tab list's header and footer
The sidebar lists the top 10 balances and the player's own rank and
balance; it is built again when a balance changed (at most every 5 s) or
every minute, and sent only to the players whose lines changed. /sidebar
off hides it, kept in board.json. The Tab list shows the server's name,
its TPS and the players on, every 5 s. An idle tick now costs a counter:
the SMP's work runs once a second, or each tick only while a teleport
warms up, a trade is open or a command's changes wait to be written.
On hooks API 43 (native_sidebar, native_tab_list, native_tick_stats). 0.9.0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 20:05:04 +01:00
levzzzandClaude Opus 5.5 95b8f15c6b veinminer 0.3.2: Nightly 2026-10-31
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 23:42:38 +05:00
levzzz be6b796a20 Merge pull request #98 from horizon-rust/remove/mod-menu
Remove mod_menu (superseded by the built-in Mods menu)
2026-10-31 23:35:44 +05:00
Ludovic MarieandKrzysix17 90e26686bd Add freelook 0.3.1
Submitted by @Krzysix17 through the form.

Submission: IvZxsJMgepBDFUkQDKiYoixlWAvyEWVFBoF6pQE1jkA
Co-authored-by: Krzysix17 <[email protected]>
2026-10-31 18:51:10 +01:00
levzzzandClaude Opus 5.5 2cd564f16b Remove mod_menu: superseded by Horizon's built-in Mods menu
The owner retires it: 0.1.1 needs an old launcher inventory feed and the game's own Mods screen now does the same job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 22:39:59 +05:00
Ludovic Marieandhakergeniusz c64087836d Add freecam 0.1.0
Submitted by @hakergeniusz through the form.

Co-authored-by: hakergeniusz <[email protected]>
2026-10-31 18:19:07 +01:00
Ludovic MarieandCayMac97 40f3d42984 Add crosshair_indicator 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 17:46:08 +01:00
Ludovic MarieandLutziano888 9a7b8a0ffc Add fair_freecam 1.2.0
Submitted by @Lutziano888 through the form.

Co-authored-by: Lutziano888 <[email protected]>
2026-10-31 17:29:18 +01:00
Ludovic Marie b571298b58 Merge branch 'feat/smp-claims' into feat/smp-grief 2026-10-31 16:49:55 +01:00
Ludovic Marie fefd3e8c6b Merge branch 'feat/smp-places' into feat/smp-claims 2026-10-31 16:49:53 +01:00
Ludovic Marie 4364dbfba3 Merge remote-tracking branch 'origin/main' into feat/smp-places 2026-10-31 16:49:46 +01:00
Ludovic Marieandumbx-dot 8c837ede87 Add freecam_plus 1.0.0
Submitted by @umbx-dot through the form.

Co-authored-by: umbx-dot <[email protected]>
2026-10-31 13:38:36 +01:00
Ludovic MarieandCayMac97 36b3c994e6 Add toggle_sprint 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 12:25:24 +01:00
Ludovic MarieandCayMac97 1ab3acaf70 Add status_effect_hud 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 12:24:07 +01:00
Ludovic MarieandCayMac97 ca4f82c665 Add light_overlay 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 12:22:54 +01:00
Ludovic MarieandCayMac97 bcf749c4d8 Add inventory_profiles 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 12:20:00 +01:00
Ludovic Marieandform1t ff69e902c8 Add menu_panorama panorama-1.0.0
Submitted by @form1t through the form.

Co-authored-by: form1t <[email protected]>
2026-10-31 11:53:17 +01:00
Ludovic MarieandCayMac97 97170b869d Add fullbright_horizon 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 11:05:09 +01:00
Ludovic MarieandCayMac97 e09a094d25 Add armor_status 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 11:02:06 +01:00
Ludovic MarieandCayMac97 c5ba9aaf03 Add appleskin_horizon 1.0.0
Submitted by @CayMac97 through the form.

Co-authored-by: CayMac97 <[email protected]>
2026-10-31 10:58:21 +01:00
levzzzandClaude Sonnet 5.5 72250afe73 Merge main into
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-31 09:18:00 +05:00
Ludovic MarieandClaude Opus 5.5 aaf6ccefcc SMP claims: pinned to horizon main with #4739 (API 22)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 00:56:05 +01:00
Ludovic MarieandClaude Opus 5.5 b0e3d98b52 SMP claims: pinned to horizon#4739's head (API 22)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 23:52:27 +01:00
Ludovic MarieandClaude Opus 5.5 803907aaa5 SMP: keep water, fire, pistons, explosions, buckets and blows out of claims (hooks 22)
On Horizon's API 22 hook points: a fluid or fire never spreads into a
claim from land of another owner (or of a player they don't trust), a
piston never pushes or pulls blocks into one, explosions spare claimed
blocks whoever lit them, buckets are guarded where they fill or empty, and
the claim's frames, armor stands, paintings, vehicles and animals
(`claims.protected_entities`) take no blows from untrusted players. The
spread and piston hooks read the open claims without allocating and never
wait on the SMP's lock. The proof adds a world_guard step on the dedicated
server, with controls outside the claim; the claims step no longer fails
when grass spreads onto its dirt. 0.8.0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 23:18:36 +01:00
Ludovic MarieandClaude Opus 5.5 ec056f9f02 SMP: land claims (/claim, /trust) on the block hooks
A player claims the chunk they stand in; only they, the players they
trust and operators break or right click blocks there
(native_destroy_block, native_right_click_block), told on the action
bar (native_action_bar, API 16). Chunks a player may hold grow with
the time played, counted by places. claims.json, written atomically.
Proved on a dedicated server: a lever, a dirt block and a placement
refused to Steve in Alex's chunk, then allowed once trusted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:08:36 +01:00
Ludovic MarieandClaude Opus 5.5 63f2fa9f01 SMP places: pinned to horizon main with #4610 (API 21)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 22:08:34 +01:00
Ludovic MarieandClaude Opus 5.5 1ec378ac96 SMP places: hook points API 21 (main's mod screens took 18 to 20)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:59:14 +01:00
Ludovic MarieandXnotCykoX 04c0ee173a Add worldedit 1.2.1
Submitted by @XnotCykoX through the form.

Co-authored-by: XnotCykoX <[email protected]>
2026-10-30 21:28:41 +01:00
Ludovic MarieandClaude Opus 5.5 0eb6ca75c3 SMP places: hook points API 18 (main took 16 and 17)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:45:55 +01:00
Ludovic MarieandClaude Opus 5.5 26079e71ed SMP places: hook points API 16, and the proof waits for each answer
#3996's block entities took API 15 on main: horizon#4610's hooks are 16.
say() polls for the command's answer instead of a fixed 1.5 s, which a
busy machine outran.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:39:31 +01:00
Ludovic MarieandClaude Opus 5.5 b9bd159dcf SMP: places (spawn, homes, warps, /tpa) and /rules
Teleports wait a warmup that a move or a hit calls off, then run vanilla's
tp as the server (native_run_command, API 15), so they cross levels.
Homes grow with the time played here, counted by the mod. places.json
holds the spawn, warps, homes and time played, written atomically.
Proved on a dedicated server with two clients: home from the Nether,
called off by a move and by damage, /tpa, warps, operators only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:24:05 +01:00
levzzzandClaude Sonnet 5.5 5b03ea2d0c Docs: drop the WebAssembly mod wording from READMEs and entry descriptions
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-30 23:08:24 +05:00
143 changed files with 23334 additions and 346 deletions

No files matched your search

+3 -1
View File
@@ -78,7 +78,9 @@ jobs:
names = zipfile.ZipFile(path).namelist()
manifest = json.loads(zipfile.ZipFile(path).read("horizon-native.json"))
assert manifest["hooks"] >= 1, manifest
assert all(n == "horizon-native.json" or (n.startswith(f"mixins/{manifest['id']}/") and n.endswith((".rs", ".toml"))) for n in names), names
bundle = f"mixins/{manifest['id']}/"
assert all(n == "horizon-native.json" or n == bundle + "LICENSE" or (n.startswith(bundle) and n.endswith((".rs", ".toml"))) for n in names), names
assert bundle + "LICENSE" in names, "the mod's LICENSE travels in its package"
print(path, manifest)
PY
- uses: actions/upload-artifact@v4
+15
View File
@@ -36,6 +36,21 @@ Submitting a project, through the form or a pull request, means you confirm that
You alone are responsible for what you publish and for any claim about it. The Horizon project may refuse, hide or
remove any project, at any time, without having to give a reason.
## Licences reviewers can read
Reviewers read a project's source before each version is listed; that is how they check it holds nothing harmful.
They may only read source under a licence that lets them, so a new project, and every new version of a listed one,
needs source under one of:
- MIT, MIT-0, BSD-2-Clause, BSD-3-Clause, 0BSD, Apache-2.0, ISC, zlib, Unlicense or CC0-1.0;
- or any licence, with a written grant from its authors that lets the Horizon reviewers read the source to review it
(say so in the pull request, with a link to the grant).
Source under GPL, LGPL, MPL or all rights reserved, without such a grant, can't be reviewed, so it can't be listed or
updated. The projects already listed under other licences when this rule was written (classic_zoom,
distant_horizons, fly_speed, serverify) stay listed as they are; their next version needs a licence above or a
grant. A port also has to follow promise 2 above.
## Copyright and takedowns
If a project in this registry uses your work without your permission:
+7 -5
View File
@@ -18,8 +18,8 @@ and the launcher to have shipped first.
**Who is responsible.** Except the projects marked Official (made by the Horizon project), everything here is made and
owned by its own authors, not by the Horizon project or its maintainers. A listing is not an endorsement. See the
[mod policy](MOD-POLICY.md): what authors promise when they submit (ports of other mods included) and how to have
a project taken down.
[mod policy](MOD-POLICY.md): what authors promise when they submit (ports of other mods included), the licences
whose source reviewers can read, and how to have a project taken down.
## Submit
@@ -41,8 +41,9 @@ cargo install --git https://github.com/horizon-rust/horizon cargo-horizon-mod
cargo horizon-mod check target/horizon-mod/my_mod.hmod
```
**Updates** come the same way: the form adds a version to your entry. Only an entry's `owners` publish its updates
without a reviewer's extra look. A published file never changes: publish a new version.
**Updates** come the same way: the form adds a version to your entry, and a reviewer reads it before it is
published. Only an entry's `owners` can add versions, and every version of a native mod is reviewed, owner or
not: its code runs with the player's account permissions. A published file never changes: publish a new version.
**Signing** is optional and recommended: put your [minisign](https://jedisct1.github.io/minisign/) public key in your
entry, and every later version must be signed with it, so nobody else can publish an update in your name.
@@ -82,7 +83,8 @@ The index: `https://horizon-rust.github.io/horizon-mods/index.json`.
[horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundles on Horizon's native hook points
([docs/native-mods.md](https://github.com/horizon-rust/horizon/blob/main/docs/native-mods.md)). Each folder keeps the
history of the repository it came from. A mod builds on any Horizon whose hook points are at least the version its
`compatibility.json` names; `horizon_revision` is the commit it was last tested on.
`compatibility.json` names; `horizon_revision` is the commit it was last tested on. `smp` and `voicechat-server`
are server mods, built into the official server's engine (see their READMEs).
```sh
python3 native/tools/native.py check zoom # apply on the tested Horizon and cargo check the game
Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+31
View File
@@ -0,0 +1,31 @@
id = "accurate_block_placement"
kind = "native-mod"
name = "Accurate Block Placement"
summary = "Hold right click with a block and every new spot you look at gets one, however fast you move."
description = """
Placing blocks fast is all about timing: vanilla places a block every four ticks while you hold right click, so moving fast skips spots and moving slowly stacks extra blocks. **Accurate Block Placement** times it for you: hold right click and every *new* spot you look at gets a block at once.
- **A new spot, a block**: bridging, building walls and floors, or sweeping along a row; no spot skipped, however fast you move.
- **No extra blocks**: staring at one spot places nothing more.
- **A single click stays a single block**: right after the click it waits as vanilla does, unless you turn the view.
- **Building backwards**: walking backwards while placing toward yourself places one block per step.
- Doors, chests, buttons and other blocks you click still work as usual.
**Settings** (Options > Mods > Accurate Block Placement): turn it on or off, bind a key to toggle it (unbound by default), and choose whether it works on servers.
Some servers see fast, accurate placing as a cheat: check their rules, or turn "Allowed on servers" off.
A port of [Accurate Block Placement: Reborn](https://modrinth.com/mod/accurate-block-placement-reborn) (MIT) by Clayborn, Flourick, schwar and KadTheHunter, rewritten in Rust for Horizon."""
authors = ["Lutziano888"]
owners = ["Lutziano888"]
license = "MIT"
source = "https://github.com/Lutziano888/horizon-accurate-block-placement"
icon = "https://raw.githubusercontent.com/Lutziano888/horizon-accurate-block-placement/main/icon.png"
[[versions]]
version = "1.0.0"
published = "2026-11-03"
game = "26.3"
file = "accurate_block_placement-1.0.0.hnmod"
sha256 = "e631745f6d267360de6eb51995ee995bf1ab3d64393d287b7b93c332b85939ce"
changelog = "First release."
+14 -1
View File
@@ -13,9 +13,22 @@ authors = ["soufi"]
owners = ["soufied"]
license = "MIT"
source = "https://github.com/soufied/AppleSkin-Horizon"
icon = "icons/appleskin.png"
icon = "https://raw.githubusercontent.com/soufied/AppleSkin-Horizon/refs/heads/main/icons/appleskin.png"
key = "RWT6eNq4e5WfVDA2lzOOi6XCm/+jJgilqT1+HUUM33ie5MCjcRfSg760"
[[versions]]
version = "0.1.2"
published = "2026-11-02"
game = "26.3"
file = "appleskin-0.1.2.hnmod"
sha256 = "627d1e7c4b4fa3dd191a4ff5887fe38b20ceb7d7d7f1197c1e03d1f25b6b6b92"
signature = """
untrusted comment: signature from minisign secret key
RUT6eNq4e5WfVJqiP4tt8vQGW6sVLSxiLXLeVunyGuyfS9aZWqPilg77biu/2B+HUDS1FlBE31F/HOhBR2YWy0GSmdwhUO7xNAs=
trusted comment: timestamp:1793633868\tfile:appleskin-0.1.2.hnmod\thashed
IW9M2qJIktmQDlARS6ZTJ1IA9BoGCfMZvatnjIlSpVPkpeG/Qpr5QKRRRPFhnZa93a5KcUG34ok+cgFxipRjBg=="""
changelog = "animation fix"
[[versions]]
version = "0.1.1"
published = "2026-10-31"
+29
View File
@@ -0,0 +1,29 @@
id = "appleskin_horizon"
kind = "native-mod"
name = "AppleSkin"
summary = "1:1 Minecraft Java parity food value preview & saturation HUD overlay."
description = """
1:1 Minecraft Java parity port of AppleSkin for the native Horizon Rust client.
Features
Saturation Overlay: Golden border and glow around active saturation drumsticks matching original icons.png pixel art.
Food Value Preview: Pulsing preview drumsticks and preview saturation border when holding edible items.
Clean Integration: Easily toggled via Options > Mods > AppleSkin."""
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/appleskin"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "appleskin_horizon-1.0.0.hnmod"
sha256 = "38a729f1aad00f6148f29578a2725cb5e7f9aea79a870fcf3cee76f39838c5ff"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTNzxiQEELascXv6v/RXLycX9k/IkVf5ggI7lL2qjyDEVxdDl5nPxbdCJ3epwwHIE6D8ZHw6QGXCFQjRz3I6Z/UA0=\r
trusted comment: timestamp:1793440355\tfile:appleskin_horizon-1.0.0.hnmod\thashed\r
6kGj1vQ1DJypzS0iDdvwksQ2FFAB1jBcycZL83OgBgQgUzbQzKRhGtleKwqAuIrRLdpQO93ale4hSMAv1z/EDg=="""
changelog = "Initial release for Horizon 26.3."
+30
View File
@@ -0,0 +1,30 @@
id = "armor_status"
kind = "native-mod"
name = "Armor Status"
summary = "Displays equipped armor & held items with authentic Java durability colors."
description = """
A native ArmorStatusHUD mod for the Horizon Rust client.
Features
Displays all 4 armor slots plus main and off-hand items on the HUD.
Authentic Java durability color coding (Green > Yellow > Orange > Red).
Item count display for non-damageable held items (torches, food, blocks).
Default position: Bottom Right (freely customizable in Options > Mods)."""
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/armor-status"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "armor_status-1.0.0.hnmod"
sha256 = "82c8672fa2c43d7946a26a560fd56fdd8f34beecf4bcb459c2d508b3d09eeee6"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTN+dR+lW2uDL5+FJfKRnHJvelJnWmhWGQeS/p0lNFVDFfq3BjZ2LCuNEtza8cZvcuKuuYF5asmxsnbtB/MMykYQA=\r
trusted comment: timestamp:1793439927\tfile:armor_status-1.0.0.hnmod\thashed\r
LTwf1kuPLucJIBfHPdx1nsp0YuHSlxbFG6PYKG2HnMxMf4acAsZbWR0QdQnzkn+7Oc/T60iQPh3X14RBST3+Bg=="""
changelog = "Initial release for Horizon 26.3."
+32
View File
@@ -0,0 +1,32 @@
id = "astral_cruiser"
kind = "native-mod"
name = "Astral Cruiser"
summary = "A huge pilotable starship with a walkable interior, lasers, dash and live portals between dimensions."
description = """
A huge starship you fly yourself, with a large walkable interior that moves with it.
- **Flight:** WASD with the ship following your view, and F5 views including the ship's own.
- **Weapons:** turrets, nose guns and two beams.
- **Travel:** coordinate dash and jump. Long dashes show only a small strip of far terrain around the ship.
- **Portals:** a portal shows the real destination dimension live, and you cross it aboard or on foot.
- **Start:** `/astral spawn`, then `/astral board`. N opens the control panel; keys are in Options > Mods > Settings.
**What it does as a native mod.** It is compiled into the game with no sandbox. It changes the game's rendering, ticking and chunk loading for the ship and portals, and registers a thread-exit callback in builds made with MinGW so the allocator frees finished threads' memory.
**What it does not do.** It adds no network access of its own beyond the game's own, and it reads no accounts or files outside the game's data. It keeps the ship in the world's own folder (`astral_cruiser/ship.json`). It holds no Minecraft code or assets.
Weapons hurt the mobs they hit. With blasting turned on in the control panel (off by default), the nose guns and beams also blow up the blocks they hit. Portals can hang in the air, so a player on foot falls if there is no ground at the exit, including into the End's void.
Built for this Horizon commit only: `68ac6f647e7022b2c696e81863d7cface28c9416`."""
authors = ["repositorykreml1n"]
owners = ["repositorykreml1n"]
license = "MIT"
source = "https://github.com/repositorykreml1n/astral-cruiser"
[[versions]]
version = "0.1.0"
published = "2026-11-01"
game = "26.3"
file = "astral_cruiser-0.1.0.hnmod"
sha256 = "69100219f92307f9190712da2854bfd43f15cf81773d89d89d6dd22411caa2f2"
changelog = "First release, built for Horizon 68ac6f6."
+73
View File
@@ -0,0 +1,73 @@
id = "basic_hud"
kind = "native-mod"
name = "The Basic HUD"
summary = "A visual HUD editor with movable widgets, FPS, coordinates, effect timers, cooldowns, mini armour and a customizable Tab list."
description = """
![The Basic HUD](https://raw.githubusercontent.com/repositorykreml1n/the-basic-hud/v0.4.1/assets/cover-0.4.1.png)
## Your HUD, your layout
Arrange 13 independent widgets directly over the game. Drag, resize and style each block with colours, opacity, padding and shadows. Use full labels or compact icons with useful values such as ×16 and 1:45.
Includes effects, consumables, durability, observed totem uses, available player-health data, item cooldowns, coordinates, FPS, ping, optional speed/CPS/clock, and mini armour next to the hotbar. Customize the Tab list with coloured numeric ping, bars, counts, sorting, scale and opacity.
Ten interface languages plus Auto. A short first-visit guide can be replayed through ?. Reset requires confirmation; leaving unsaved edits offers Save / Discard changes / Stay. Failed writes retain the draft.
## Install and compatibility
Native .hnmod for Minecraft 26.3, pinned to Horizon **68ac6f647e7022b2c696e81863d7cface28c9416**. Install through the launcher and select **Performance**. Dev can overflow the startup stack on this Windows baseline. Other Nightlies require separate verification.
Client-side only. Unknown player health remains unknown, unseen totem uses cannot be counted and server-plugin-only timers are unavailable. No server mod is required. No FPS improvement is promised. Native mods compile into the game and run with the player’s rights, without a sandbox. Tested on Windows; other platforms were not tested.
## Other languages
**Русский:** редактор поверх игры, размер и оформление каждого блока, значки с числами, FPS, координаты, кулдауны, мини-броня и настройка Tab. Обучение один раз; подтверждение сброса и выхода с правками.
**Українська:** редактор поверх гри, розмір і стиль кожного віджета, значки зі значеннями, FPS, координати, час відновлення, мініброня та налаштування Tab. Навчання під час першого входу й захист незбережених змін.
**Deutsch:** Editor direkt im Spiel, frei verschiebbare und skalierbare Widgets, Symbole mit Werten, FPS, Koordinaten, Abklingzeiten, Rüstungsanzeige und anpassbare Tab-Liste. Einführung beim ersten Besuch und Schutz ungespeicherter Änderungen.
**Français :** éditeur superposé au jeu, widgets déplaçables et redimensionnables, icônes avec valeurs, FPS, coordonnées, délais de récupération, petite vue de l’armure et liste Tab personnalisable. Tutoriel initial et protection des modifications non enregistrées.
**Español:** editor superpuesto al juego, widgets movibles y ajustables, iconos con valores, FPS, coordenadas, tiempos de recarga, vista pequeña de la armadura y lista Tab personalizable. Tutorial inicial y protección de cambios sin guardar.
[Full update notes in six languages](https://github.com/repositorykreml1n/the-basic-hud/blob/main/CHANGELOG.md)
MIT. Not an official Horizon, Mojang or Microsoft product. Cover is a generated promotional illustration, not a gameplay screenshot or measured FPS claim."""
authors = ["repositorykreml1n"]
owners = ["repositorykreml1n"]
license = "MIT"
source = "https://github.com/repositorykreml1n/the-basic-hud"
icon = "https://raw.githubusercontent.com/repositorykreml1n/the-basic-hud/main/icon.png"
[[versions]]
version = "0.4.1"
published = "2026-11-03"
game = "26.3"
file = "basic_hud-0.4.1.hnmod"
sha256 = "36431d3a2d810e2aefbd8611767a80f9184e3df032e4cc2b9b9fd9398f794560"
changelog = """
Major update from 0.1.0 to 0.4.1.
- Visual in-game editor: drag, individual scale, colours, opacity, shadows, padding and snap.
- 13 widgets, including FPS, coordinates, available player health and actual item cooldowns.
- Icons + values: ×16, 1:45 and durability without long labels.
- Mini armour beside the hotbar, including elytra.
- Custom Tab list: numeric ping, bars, player counts, sorting and scale.
- Smoothed optional speed; single-player ping labelled Local.
- Ten UI languages plus Auto; first-visit tutorial, replay through ?.
- Reset confirmation; Save / Discard changes / Stay on leaving with edits; failed-save recovery.
Pinned to Minecraft 26.3 / Horizon 68ac6f6. Use the Performance build profile on Windows; Dev can overflow the startup stack on this baseline. No compatibility with other Nightlies or FPS gain is claimed.
Validated: 67 Rust test executions, native build, isolated gameplay/editor tests, restart persistence and ordinary asynchronous startup.
Full notes (English + Russian, Ukrainian, German, French and Spanish summaries): https://github.com/repositorykreml1n/the-basic-hud/blob/main/CHANGELOG.md"""
[[versions]]
version = "0.1.0"
published = "2026-11-01"
game = "26.3"
file = "basic_hud-0.1.0.hnmod"
sha256 = "fe2c5d9da681b4f575bff867df70d6e2f61614b0215a206fb7a0c3b0027db994"
changelog = "First release."
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+10 -2
View File
@@ -1,14 +1,22 @@
id = "blockscope"
kind = "native-mod"
name = "Blockscope"
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."
summary = "The block under your crosshair: its name, id, distance and icon, plus hardness, states and a container's slots."
description = "A Jade-style tooltip for Horizon: the block under your crosshair with its name in your language, block id, distance and icon, plus its hardness and the tool it wants, its state properties, a crop's growth and, for a container, its slots. Ten switches on its own settings page."
authors = ["Domanffe"]
owners = ["Domanffe"]
license = "MIT"
source = "https://github.com/Domanffe/blockscope"
icon = "https://horizon-rust.github.io/horizon-mods/icons/blockscope.png"
[[versions]]
version = "0.3.1"
published = "2026-11-02"
game = "26.3"
file = "blockscope-0.3.1.hnmod"
sha256 = "4fea94e5337aa82df3d981fc6231fecc80161262add577b596bb6661e0e303d1"
changelog = "Rebuilt on the 2026-11-01 Nightly (Horizon `4aa6854`). All five hook points are named now — `mods`, `settings`, `settings_click`, `hud`, `tick` — so the bundle still applies when the game moves them."
[[versions]]
version = "0.3.0"
published = "2026-10-30"
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+9 -6
View File
@@ -2,18 +2,21 @@ id = "chatheads"
kind = "native-mod"
name = "ChatHeads"
summary = "A player's head left of every chat line they wrote."
description = """
Each chat line from a player gets their 8x8 skin face in the strip to its left, like the Chat Heads Java mod. Lines without a player (system messages, /me, wrapped continuations) stay as they are; the face fades with its line.
Settings: Options > Mods > ChatHeads > Settings (on or off).
Known limit: the chat's hit-test does not know about the 10 px indent, so clicking a link in chat lands 10 px left of its pixels."""
description = "Each chat line from a player gets their 8x8 skin face in the strip to its left; lines without a player stay as they are, and the face fades with its line. The switch lives in Options > Mods."
authors = ["Domanffe"]
owners = ["Domanffe"]
license = "MIT"
source = "https://github.com/Domanffe/chatheads"
icon = "https://horizon-rust.github.io/horizon-mods/icons/chatheads.png"
[[versions]]
version = "0.1.1"
published = "2026-11-02"
game = "26.3"
file = "chatheads-0.1.1.hnmod"
sha256 = "16f5e1db33255c550c087fcf3ed982a4be9abf61b547b470a08f471d309ad1fe"
changelog = "Rebuilt on the 2026-11-01 Nightly (Horizon `4aa6854`). The three hook points it shares — `mods`, `settings`, `settings_click` — are named now instead of pinned to raw source targets, so a Horizon update that moves them no longer breaks the bundle."
[[versions]]
version = "0.1.0"
published = "2026-10-30"
+41
View File
@@ -0,0 +1,41 @@
id = "chop"
kind = "native-mod"
name = "Chop!"
summary = "Cut one log and the whole tree tips over, crashes down and breaks apart, with soft vanilla sounds and particles."
description = """
Cut one log with an axe: the whole tree tips over like a real falling tree, crashes to the ground, and breaks apart from the base to the top in little pops. Everything is made of vanilla sounds and particles: no resource pack.
- **The log shakes while you cut it**, then pops when it gives way.
- **A physical fall**: the trunk drops onto the stump, creaks, tips over with gravity and stops where it meets the ground. Leaves in the way are crushed.
- **Falls in front of you**, within an adjustable angle, toward the clearest side.
- **A cute break-up** from the base to the top, with clouds, sparkles and hearts.
- **Replants a sapling** of the tree's kind (from its loot, or always).
- **Chain reactions**: a falling trunk can knock over the next tree.
- **Adjustable damage**: hurts monsters by default; animals, allies and players are spared unless you turn them on.
- Vanilla loot (Fortune, Silk Touch), honest axe wear, fall preview, shaking trees with an empty hand, giant mushrooms and Nether fungi, angry bees, birds, apples, biome-tinted leaves, optional loot into your inventory.
- **Everything is configurable in game**, with commands or by clicking in the chat: `/chop config` (operators), `/chop perso` (each player's sounds, particles and preview), `/chop stats`, `/chop aide`. The menus are in French.
**What it does and doesn't do.** It runs server-side, where the world runs: players who join install nothing. No network access, no telemetry, never runs a program. It reads and writes one file, `<world>/data/chop.txt` (settings, stats, player-placed logs). It only touches trees, the sapling it replants, its own `block_display` entities and what a falling tree hits. Besides the game's functions, it runs a few vanilla commands as the server: `kill` on its own tagged displays, `item modify` for the axe's wear, `damage` on a player the tree hits (off by default), and `summon item` / `setblock … keep` for loot and replanting when the tree is in another dimension.
Native source mod: install it on the launcher's Mods page, press Build there, and confirm the native-code warning. The launcher builds a separate game with it, and it runs with your account's permissions. Each version is pinned to one Horizon Nightly and Horizon Mixins revision, named in its changelog; the launcher builds those exact revisions.
Original mod by Mayonesu0. All rights reserved, except the grants in its [LICENSE](https://github.com/Mayonesu0/chop/blob/main/LICENSE): players may use it unchanged, Horizon's registry may distribute it, and Horizon's reviewers may read it to review it."""
authors = ["Mayonesu0", "Claude"]
owners = ["Mayonesu0"]
license = "LicenseRef-Chop-1.0"
source = "https://github.com/Mayonesu0/chop"
icon = "https://raw.githubusercontent.com/Mayonesu0/chop/main/registry/icon.png"
key = "RWSwx1v4zUCUjQ8rssfZCXvXkvgVc9TbA60RJyCtVCeMx4lSTanIleIs"
[[versions]]
version = "1.0.1"
published = "2026-11-03"
game = "26.3"
file = "chop-1.0.1.hnmod"
sha256 = "5abac195c54da7716f35f641489c0dba780444c91b0b3e4f3387ecd4cb2b260b"
signature = """
untrusted comment: signature from minisign secret key\r
RUSwx1v4zUCUjfR+WYS97be0UecaBcKGyNXNsCQAPVG0j2U2yeMfnNPkWMEU9CMOobMI6nKaPj/Kqv0EPAuw2xiPnRaduNgI8wA=\r
trusted comment: timestamp:1793711491\tfile:chop-1.0.1.hnmod\thashed\r
eOi2t+LMQ4LOM/nsJqmpvAraWsLcgdRZA4OJotjBkgxcANkG3MeZ9tbAKbEaidIy1kBSZyc4Dri+3MtOsd+xBA=="""
changelog = "First release (1.0.0 was never published: its package carried a LICENSE file the registry's checker refuses; the code is the same). Pinned to Horizon Nightly 2026-11-01 (c9d474d) and Horizon Mixins ddd9700; the launcher builds those exact revisions."
+1 -1
View File
@@ -32,7 +32,7 @@ Both can be installed at once: they use different keys.
- Written from scratch for Horizon's mod API. Not affiliated with Wurst-Imperium.
- Some servers forbid zoom mods: check the rules before you join.
**Now a native mod (2.0.0)**: the launcher builds it into the game (Settings → Game → Native mods (experimental), then Build). Native mods are not sandboxed and run with your account's permissions, so read the source before you build it: it is one small bundle on Horizon's native hook points (key, wheel, zoom, mods list). Tested against Horizon `6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a` with Horizon Mixins `0d6b103c70633d8fd9443d8d0acd3aadaacedb06`. A key set in 1.0.0 carries over. 1.0.0 is the WebAssembly version.
**Now a native mod (2.0.0)**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). Native mods are not sandboxed and run with your account's permissions, so read the source before you build it: it is one small bundle on Horizon's native hook points (key, wheel, zoom, mods list). Tested against Horizon `6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a` with Horizon Mixins `0d6b103c70633d8fd9443d8d0acd3aadaacedb06`. A key set in 1.0.0 carries over.
Source and issues: [github.com/SU4G3/classic-zoom](https://github.com/SU4G3/classic-zoom)"""
authors = ["Surge"]
+1 -1
View File
@@ -11,7 +11,7 @@ Your block position and the direction you face in the top-left corner of the scr
It reads the player's position, draws on the HUD and writes to chat: 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). It was the WebAssembly SDK's example mod; the time of
(install it on the launcher's Mods page, then press Build there). The time of
day line is gone, since Horizon's hook points don't give the game's clock yet. Turn it off in Options > Mods: it
rests at once, and the next build leaves it out. Its source is
[horizon-mods/native/coords-hud](https://github.com/horizon-rust/horizon-mods/tree/main/native/coords-hud).
+33
View File
@@ -0,0 +1,33 @@
id = "crosshair_indicator"
kind = "native-mod"
name = "Crosshair Indicator"
summary = "Dynamic target reach awareness: visual feedback on your crosshair when hovering over an entity in attack range."
description = """
Crosshair Indicator
A native Horizon mod by CayMac that ports the popular Java Crosshair Indicator mod into Horizon:
Dynamic Target Reach Awareness: Real-time visual feedback directly on your crosshair when hovering over an entity in attack reach (<= 3.0 blocks).
Iconic PvP Target Reticle: Default crisp white corner brackets [ + ] hugging the crosshair, matching classic competitive PvP clients.
Multiple Styles: Choose between Brackets [ + ], Center Dot, Diamond, and Tactical Chevron.
Custom Color Themes: Classic White (Default), Dynamic (Green/Orange cooldown awareness), Combat Red, Pink Aesthetic, Diamond Cyan, and Gold.
Cooldown Bar: Optional subtle weapon cooldown progress indicator below the crosshair.
Configurable: Easily customize styles, themes, and keybinds in Options > Mods > Crosshair Indicator."""
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/crosshair-indicator"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "crosshair_indicator-1.0.0.hnmod"
sha256 = "96beba1f7c94fca492b552fb12af0d20047dc1799f0801607841d56b12786226"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTNz67XDjUwaJ8if7+jDk+qXnJLdhtyjh8mk04UuZ2bme/Tzcpi1Od4ppYmvqzfZ+rEAeG2hb9Lytn+J2nFy1u8gc=\r
trusted comment: timestamp:1793461576\tfile:crosshair_indicator-1.0.0.hnmod\thashed\r
zF7EJHDjJPwVG+cYcSgvGJv4kjjYRMdMZipUQyABH0DEMIZ2Fe13hb2nbYYRZlNDG1j4kjhgEYQTODFiAlaCBQ=="""
changelog = "Initial release for 26.3 Horizon."
+41
View File
@@ -0,0 +1,41 @@
id = "crossversion"
kind = "native-mod"
name = "Cross Version"
summary = "Allows version hopping offline."
description = """
Hop versions, offline.
Uses ViaProxy as an inbetween for now."""
authors = ["JustKarkat"]
owners = ["JustKarkat"]
license = "MIT"
source = "https://github.com/JustKarkat/horizon-crossversion/"
icon = "https://github.com/JustKarkat/horizon-crossversion/blob/master/icon.png?raw=true"
key = "RWTcPJHJYjPW0pV3lPwr45crCu0wJxW5HbDLc3fr3Eom1TzxO++kHOo4"
[[versions]]
version = "0.8.0"
published = "2026-11-03"
game = "26.3"
file = "crossversion-0.8.0.hnmod"
sha256 = "5b371f1b7b3582e0c92aa1dc22b491b50c024fe95833aa2bd9457189631caafd"
signature = """
untrusted comment: signature from rsign secret key
RUTcPJHJYjPW0vc8h9pG+LC/9EkYg60UM28gNkxJ31iDOXapyV80qqnB9H12OuOtzBmPkbB33fkh0IKE5O8zcCzat0jyj6Tpqw8=
trusted comment: timestamp:1793679858\tfile:crossversion-0.8.0.hnmod\thashed
3hY5NK517bzva/2q54Pmj9QXO3z6WF0WWMfuA8T9H8qCuOe6TYnmYB5eEagRMpZUNK/ICcrfPEx+DE303XIOAQ=="""
changelog = "Offline mode only. "
[[versions]]
version = "0.7.1"
published = "2026-11-02"
game = "26.3"
file = "crossversion-0.7.1.hnmod"
sha256 = "deb9cf8a84838e6c48999f70fbd3c81db90992c456d6e85a0e0746b088c5b8fa"
signature = """
untrusted comment: signature from minisign secret key
RUTcPJHJYjPW0nmeON4u6rxOS7AxK+s/e3kPaLkJ3lm618iPPc8oP8nWdJQ8k9yYriSZSu091TZ5m7qX56nNovAjeZyTcOdrUAk=
trusted comment: timestamp:1793657606\tfile:crossversion-0.7.1.hnmod\thashed
q2Oy6yVv5T+GHc2iM+HxNDImfPcaKGV/XPToNCTQFHSTKJ8+rz5IV4LfJnOBwBp1rZQZoGkExopDrXfZyQybCQ=="""
changelog = """
Version: 0.7.1
Changelog: The MIT notice now ships inside the .hnmod (it is the header of the bundled engine source). The settings page's 'Delete the downloaded ViaProxy' deletes the jars only, so the account added to ViaProxy and its refresh token in saves.json stay. Also: a version switch on Edit Server and Direct Connect: choose what version ViaProxy tells that server it is (auto, 1.8, 1.9, 1.12.2, 1.16.5, 1.20.1, 1.21.1, 1.21.4, 26.3), kept per address, for servers whose status reports the wrong version."""
+28
View File
@@ -0,0 +1,28 @@
id = "customhud"
kind = "native-mod"
name = "Beautiful Day Counter"
summary = "A lightweight day counter, clock and day/night cycle bar for the HUD, with an in-game settings screen."
description = """
Beautiful Day Counter for Horizon, a native mod.
Shows the current survival day, an in-game clock and a color-shifting bar that follows the day/night cycle, without changing gameplay. It is a port of the author's own Fabric/NeoForge mod [CustomHUD](https://github.com/sonoFrangu/CustomHUD), with the same HUD and the same settings screen.
- Day counter with the game mode icon; a golden apple or totem, a rainbow counter and a sound on milestone days (50, 100, 365, every 100).
- 12-hour or 24-hour clock; the clock icon turns into a bed when you can sleep.
- Day/night bar with four color themes (Classic, Pastel, Colorblind, Mono), night marks, countdown to night or day, moon phase.
- Top or bottom placement, size, opacity, offsets, optional background; vanilla boss bars move below the HUD when it is at the top.
- Settings screen opened with H (or from Options > Mods): Elements, Look and Behavior, with tooltips.
- English, Italian, German, Spanish, French and Portuguese."""
authors = ["SCARABOCCHIO100"]
owners = ["sonoFrangu"]
license = "MIT"
source = "https://github.com/sonoFrangu/CustomHUD-Horizon"
icon = "icons/customhud.png"
[[versions]]
version = "1.6.0"
published = "2026-10-31"
game = "26.3"
file = "customhud-1.6.0.hnmod"
sha256 = "ab255d3c722b40f4d068a762857d6c027c11381308bfe23196e2974307814a7f"
changelog = "First release for Horizon: the CustomHUD 1.6.0 HUD and settings screen as a native mod."
+1 -1
View File
@@ -12,7 +12,7 @@ An unofficial Rust port of [Distant Horizons](https://gitlab.com/distant-horizon
Settings: Options > Mods > Distant Horizons. In chat: `/dh` for status, `/dh distance 512`, `/dh quality high`, `/dh debug on` and more (Tab completes them).
Native source mod: enable Settings → Game → Native mods (experimental), choose Build and confirm the native-code warning. The launcher downloads its own Rust and builds a separate game (10–30 minutes the first time). It runs with your account's permissions.
Native source mod: install it on the launcher's Mods page, press Build there and confirm the native-code warning. The launcher downloads its own Rust and builds a separate game (10–30 minutes the first time). It runs with your account's permissions.
Pinned to Horizon nightly `d009ebd140476b5bdc958c00f56a75e82abec691` (2026-10-29) and Horizon Mixins `0d6b103c70633d8fd9443d8d0acd3aadaacedb06`; the launcher builds these exact revisions. Other Horizon revisions have not been validated. It builds together with Horizon's official native mods; it can't be built with a native mod pinned to a different Horizon commit. Turn Video Settings > LOD Distance off: Horizon's own far terrain draws in the same place.
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+34
View File
@@ -0,0 +1,34 @@
id = "fair_freecam"
kind = "native-mod"
name = "Fair Freecam"
summary = "Fly the camera freely while your player stays where it is, with an optional fair play mode."
description = """
Press **F4** to leave your body and fly the camera anywhere. Your player stays where it is and you see it from outside. Press **F4** again to snap back.
**Controls while flying:**
- **W A S D**: fly where you look
- **Space / Shift**: up / down
- **Ctrl**: 2.5x faster
- **Mouse wheel**: change the speed
**Settings** (Options > Mods > Fair Freecam):
- **Speed**: 2, 5, 10, 20, 40 or 80 blocks per second.
- **Fair play**: the camera can't fly through blocks, so no peeking into caves or bases through walls. Glass, fences and doors stop it, grass and flowers don't.
- **Allowed on servers**: turn it off to only use freecam in your own worlds.
While flying, your player doesn't move, attack, mine or use items.
Some servers don't allow freecam, so check their rules first."""
authors = ["Lutziano888"]
owners = ["Lutziano888"]
license = "MIT"
source = "https://github.com/Lutziano888/horizon-freecam"
icon = "icons/fair_freecam.png"
[[versions]]
version = "1.2.0"
published = "2026-10-31"
game = "26.3"
file = "fair_freecam-1.2.0.hnmod"
sha256 = "4fdd0b3067a00d23fe30ee4d594293ef0705325bb5e6e5b292f90c1e8b8a5f8a"
changelog = "First release as Fair Freecam (id fair_freecam), so it doesn't clash with other freecam mods."
+24
View File
@@ -0,0 +1,24 @@
id = "flair"
kind = "native-mod"
name = "flair"
summary = "Custom accessory mod"
description = "A cosmetics catalog for Horizon. Open it from the title screen or the pause menu and wear HD capes that move like cloth, animated bandanas with Japanese script, hats, wings, shoulder pets and auras with colour themes. Turn your player in a live preview, hide your armor, and add your own capes and Blockbench cosmetics with animations. Only you see them."
authors = ["_idom_"]
owners = ["umbx-dot"]
license = "MIT"
source = "https://github.com/umbx-dot/horizon-flair"
icon = "https://raw.githubusercontent.com/umbx-dot/horizon-flair/main/icon.png"
key = "RWT9Ujo/oCprPGHHiSLHFNOJV1ZzoJdAU6EdZyiQcBwT2iMXfiYms4hD"
[[versions]]
version = "1.0.0"
published = "2026-11-03"
game = "26.3"
file = "flair-1.0.0.hnmod"
sha256 = "64e4d645d5bda3c363603d483af2095d6d3673d91803954f338d54265bf4fab5"
signature = """
untrusted comment: signature from minisign secret key\r
RUT9Ujo/oCprPBIbqV9rmrjRgpF9gnCIL+4FlZXSTq0Qi4kkklSKfjNDx9YFIw1DuufGVSL3+lOieSX4Fx0nRl7j/CCUNjiWIQs=\r
trusted comment: timestamp:1793723044\tfile:flair-1.0.0.hnmod\thashed\r
QPCGNEAWFCaZLdZLgkUVY9v+hRP4/gJe4H2hCMh7YT7k/V4tynkQ0AM3hZqEdrBoPn4q5SFHDVIcloGkqCTzDw=="""
changelog = "Initial Release."
+1 -1
View File
@@ -12,7 +12,7 @@ Horizon's own FPS counter, formerly an extra in Options > Horizon Extras.
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
(install it on the launcher's Mods page, then press Build there). 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).
"""
+23
View File
@@ -0,0 +1,23 @@
id = "freecam"
kind = "native-mod"
name = "Freecam"
summary = "Freecam mod ported for Horizon"
description = "A rewrite for Horizon of the Freecam mod by the Freecam authors (https://github.com/MinecraftFreecam/Freecam, MIT; on Modrinth as freecam). Its licence notice is kept in the source."
authors = ["hakergeniusz"]
owners = ["hakergeniusz"]
license = "MIT"
source = "https://github.com/hakergeniusz/freecam"
key = "RWR4cJC6Jj3EWGi26o29Rs3hAIPWqf8koFdXV4bBLF/Tm84QPfRAYjO5"
[[versions]]
version = "0.1.0"
published = "2026-10-31"
game = "26.3"
file = "freecam-0.1.0.hnmod"
sha256 = "6314ed87d3b58ed0244bed388201c64bffb8deab4f5ff7beed2a3791b391a6da"
signature = """
untrusted comment: signature from rsign secret key
RUR4cJC6Jj3EWN7yDLuEFss9Xir36oWeHI3yyIK+PlIffoXzHXcZ4eBAN/UcOwGpniH+lJl9uU1vzg8yRe9i6SKNMFhI6qbHkAQ=
trusted comment: freecam 0.1.0 for Horizon (Minecraft 26.3), built from github.com/hakergeniusz/freecam commit 78d8f05
6sfFvM54vZFE+02rwu0EJVbCnv5dJAJ7PDEWp64QnyEjlWYEnzXDzDAyLzKdZAfXwtvX+igaCS2KJKCk5UY5CA=="""
changelog = "Release."
+27
View File
@@ -0,0 +1,27 @@
id = "freecam_plus"
kind = "native-mod"
name = "Freecam+"
summary = "Fly the camera anywhere while your player stays perfectly still."
description = """
Press U and your camera leaves your body. Fly it with WASD, Space and Shift, hold Ctrl to go faster and use the mouse wheel to change the speed. Your player does not move or turn, so the server sees you standing still. Through walls and speed can be changed in Options, Mods, Freecam.
Since this is a native mod it will not work in v0.2.97 as there are no native hooks possible. This works on all nightly releases and LIKELY on version upwards from v0.2.97."""
authors = ["_idom_"]
owners = ["umbx-dot"]
license = "MIT"
source = "https://github.com/umbx-dot/horizon-freecam"
icon = "icons/freecam_plus.png"
key = "RWT9Ujo/oCprPGHHiSLHFNOJV1ZzoJdAU6EdZyiQcBwT2iMXfiYms4hD"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "freecam_plus-1.0.0.hnmod"
sha256 = "264218191c32e7107982eab297b30a43076c5f1467d4e292cb844213258b5eb1"
signature = """
untrusted comment: signature from minisign secret key\r
RUT9Ujo/oCprPAR7aucj7Ez3exPIc7MGDVH8ns9azj3ilxe+G4JTWb0PnF0YYxAm0FQJJs3DcmHxHjEEfiiWymsAnTG/cdggrQI=\r
trusted comment: timestamp:1793450278\tfile:freecam_plus-1.0.0.hnmod\thashed\r
/4Di5+zUUSOmYB56u/GDYran5upJjtmfdUff5uKER6lknPGYjsgeLfENH8trB+d8g2hhn70b5+lGxy9mrBfJBg=="""
changelog = "Initial Release. "
+19
View File
@@ -0,0 +1,19 @@
id = "freelook"
kind = "native-mod"
name = "Freelook"
summary = "Hold a key for a third person camera you can look around in"
description = """
Freelook is a Freelook/Perspective Mod for Fabric, allowing you to move your camera freely without changing your player's rotation.
Camera distance can be adjusted by scrolling."""
authors = ["Krzysix17"]
owners = ["Krzysix17"]
license = "MIT"
source = "https://github.com/Krzysix17/horizon-freelook"
[[versions]]
version = "0.3.1"
published = "2026-10-31"
game = "26.3"
file = "freelook-0.3.1.hnmod"
sha256 = "9c0c92ce9f0fc8e7bc1bd34349e26d42b32b8abd2c7a09ec710936f9b6ce9aaf"
changelog = "First release"
+1 -1
View File
@@ -3,7 +3,7 @@ kind = "native-mod"
name = "MoreGamma"
summary = "Brightness past 100%: set your own gamma with /gamma, or toggle full brightness with /fullbright."
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. Uses only Horizon's native hook points (hooks 1); tested on Horizon 41672b1 and horizon-mixins 0d6b103.
A native Horizon mod: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Uses only Horizon's native hook points (hooks 1); tested on Horizon 41672b1 and horizon-mixins 0d6b103.
See in the dark without torches or night vision. MoreGamma lets you set the brightness (gamma) far past the video settings' 100% "Bright" limit, with a chat command, and switch full brightness on and off.
+29
View File
@@ -0,0 +1,29 @@
id = "fullbright_horizon"
kind = "native-mod"
name = "Fullbright"
summary = "Toggle maximum 1000% gamma cave and night brightness with smooth fade via keybind G."
description = """
Maximum brightness and night illumination for the Horizon Rust client.
Features
Press G (rebindable in Options > Mods > Fullbright) to immediately toggle 1000% Gamma cave and night illumination without shadows.
Smooth 250ms brightness fade transition and HUD status notification.
100% client-side lightmap modification, safe for multiplayer."""
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/fullbright"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "fullbright_horizon-1.0.0.hnmod"
sha256 = "db6175eec1711dbc5ab2a2edc40eee9cbc4080ec1d706a3cef688e54ba9cb017"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTN70A+lNWzZF2M2MiOMfjOIqVnTGNY05bQXW47Ov/mHp8io8o9HZiOYQXOYbxhazjWOfGU3WWuRzqj05swkoG/QY=\r
trusted comment: timestamp:1793440355\tfile:fullbright_horizon-1.0.0.hnmod\thashed\r
mJGiv7YnJHRGreiX5K3mx6zwEtRTC4rAygoIC+rkp1tjUeazIwyehtojb6T+iQBQaq2p97PTor410KnbK675BA=="""
changelog = "Initial release for Horizon 26.3."
+25
View File
@@ -0,0 +1,25 @@
id = "fullbright_nofog"
kind = "native-mod"
name = "Fullbright & NoFog"
summary = "Toggle full brightness and the world's fog with a key (G / N), /fullbright or /nofog."
description = """
A native Horizon mod: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Uses only Horizon's native hook points (hooks 80, for `render_fog`); tested on Nightly 2026-11-01 (c9d474d) and horizon-mixins 0d6b103.
- **Fullbright**: press G (or `/fullbright`, `/fb`) to toggle full brightness (gamma 1500%). Your brightness slider and `options.txt` are never changed; turning it off gives your own brightness back.
- **NoFog**: press N (or `/nofog`) to remove the world's fog. `/nofog fluids` also clears water, lava and powder snow fog.
- `/fullbright on|off` and `/nofog on|off` set them directly.
- Options → Mods → Fullbright & NoFog: a button for each toggle and both keys, rebindable.
Toggling is silent (no chat messages). Client-side: works in single player, on LAN and on servers. Settings are remembered between sessions."""
authors = ["Nirokx"]
owners = ["Nirokx"]
license = "MIT"
source = "https://github.com/Nirokx/horizon-fullbright-nofog"
[[versions]]
version = "1.0.0"
published = "2026-11-02"
game = "26.3"
file = "fullbright_nofog-1.0.0.hnmod"
sha256 = "07a13abd8af2405343241f8985db7595a03bfb8ced2f1dfab8a105848eb1b7d7"
changelog = "First release."
+38
View File
@@ -0,0 +1,38 @@
id = "hitbox_plus"
kind = "native-mod"
name = "Hitbox+"
summary = "Customizable entity hitboxes with exact weapon & spear in-reach colors, thickness, eye rays, and sprint HUD."
description = """
Hitbox+ for Horizon
Hitbox+ is a high-performance native mod ported to Horizon that revamps vanilla F3+B debug hitboxes with full customization and PvP reach indicators.
Key Features:
Dynamic In-Reach Detection: Hitboxes automatically change to your chosen reach color when your crosshair is within hitting distance.
Spear Reach: Accurately extends reach to 4.5 blocks (survival) and 6.5 blocks (creative).
Melee Reach: Standard 3.0 blocks for swords, axes, maces, and fists.
11 Selectable Reach Colors: Choose between Bright Green, Neon Cyan, Aqua Mint, Gold Yellow, Sunset Orange, Hot Pink, Neon Purple, Electric Blue, Crimson Red, Pure White, or smooth dynamic Rainbow.
Hitbox Styles & Thickness: Custom styles (White, Cyan, Yellow, Red, Rainbow) and 3 thickness modes (1.5px Thin, 2.5px Default, 4.0px Thick).
Eye-Ray & Looking Direction: Toggle looking direction lines and forehead level markers.
Always-On Sprint HUD: Built-in sprint status indicator with customizable screen corner placement (Top-Left, Top-Right, Bottom-Left, Bottom-Right).
Zero FPS Cost: Pure lock-free atomic architecture.
Controls:
Press H to toggle hitboxes on/off.
Access Options > Mods > Hitbox+ to customize all colors, thickness, and HUD positions."""
authors = ["taxtii", "itzglecni"]
owners = ["taxtii"]
license = "MIT"
source = "https://github.com/taxtii/hitbox_plus.git"
[[versions]]
version = "1.3.0"
published = "2026-11-03"
game = "26.3"
file = "hitbox_plus-1.3.0.hnmod"
sha256 = "8d1b95e7461e38fc40994b35a82c1f753a58e9751ce27af1dc101a4717dba8a4"
changelog = """
- Added dynamic spear attack reach support (4.5 blocks in survival, 6.5 blocks in creative with 0.125 hitbox margin).
- Standard melee weapons (swords, axes, fists) use exact 3.0-block in-reach detection.
- Added 11 selectable reach color options (Bright Green, Neon Cyan, Aqua Mint, Gold Yellow, Sunset Orange, Hot Pink, Neon Purple, Electric Blue, Crimson Red, Pure White, Rainbow).
- Removed red hit flashing when attacking entities.
- Zero-overhead lock-free atomic rendering with configurable line thickness, eye rays, and always-on toggle sprint HUD."""
+31
View File
@@ -0,0 +1,31 @@
id = "horizon_physics"
kind = "native-mod"
name = "Horizon Physics"
summary = "Physics for blocks, mobs, items, snow, water, smoke, capes and more, with its own settings screen."
description = """
By tariksabanuz. Physics for Horizon in the spirit of PhysicsMod: broken blocks fall apart into pieces, mobs die as
ragdolls, snow gives way under your feet, water waves, smoke rises and plants sway in the wind.
- Broken blocks crumble into pieces (blocky, particles, crumbled voxel or fractured). Mobs die as ragdolls or come apart.
- Structures and trees fall when what holds them up is broken. Dropped items tumble and settle.
- Snow layers are pressed down by you and the mobs, water has waves that boats (and you in them) ride, and leaves, plants and vines sway.
- Hit a ragdoll or a piece and it flies off; grass and flowers bend away as you walk through them.
- Fire, campfires and lava give off smoke and embers that gather and darken under ceilings, capes and banners are cloth, doors swing open and shut, leads and fishing lines are ropes.
- Rain splashes and rings the water, snow falls, and the wind blows harder with the weather.
- A Physics Settings button in Options opens a screen with a page for every part, and a Quick Setup the first time you start.
Needs the Performance build in the launcher. Inspired by PhysicsMod by Haubna; this is a clean-room version for
Horizon and contains none of its code or assets. It uses your own game's textures at runtime, and the icon is my own."""
authors = ["tariksabanuz"]
owners = ["tariksabanuz"]
license = "MIT"
source = "https://github.com/tariksabanuz/PhysicsModHorizon"
icon = "icons/horizon_physics.png"
[[versions]]
version = "0.15.1"
published = "2026-11-03"
game = "26.3"
file = "horizon_physics-0.15.1.hnmod"
sha256 = "d7cf1201f0a47a852aa1a4c9684ef9e658ec3c86852104d44716d05f11281ca3"
changelog = "First release: block, mob, item, snow, water, smoke, cloth, weather and collapse physics, with a Physics Settings screen."
+64
View File
@@ -0,0 +1,64 @@
id = "horizonmatica"
kind = "native-mod"
name = "Horizonmatica"
summary = "Litematica for Horizon: schematics as ghost blocks, a verifier, material lists and a printer that builds the missing blocks for you."
description = """
Horizonmatica is a re-creation of **Litematica** (with its **printer**) for Horizon. Load a schematic, see it as ghost blocks in your world, check your build against it, get a material list, and let the printer place the missing blocks.
## Features
- **Formats:** `.litematic` (versions 1–7, several regions), `.schem` (Sponge v1–v3: WorldEdit, FAWE, Amulet, Axiom), `.schematic` (MCEdit / WorldEdit up to 1.12) and vanilla structure `.nbt`. Older files are upgraded with Horizon's data fixer, the same way old worlds are.
- **Overlay:** missing blocks as translucent real block models (chests, banners, heads and other block entities included), optionally visible through walls.
- **Verifier:** wrong block (red), wrong state (orange), extra block (pink), plus a progress HUD.
- **Layers:** all / one layer / above / below / range.
- **Placement:** move, rotate, mirror; a move mode for mouse wheel and arrow keys. Each placement is remembered per world and dimension and restored when you join again.
- **Material list:** missing, total and carried, with item icons, in the HUD, in chat or in the menu.
- **Menu (M):** a screen of its own, drawn with the game's widgets.
- **Printer:** places one missing block within reach every few ticks, from the bottom up. It only clicks when Horizon's own placement logic says the click produces exactly the wanted state (stairs, slabs, logs, furnaces…). Redstone-aware order, repeater, comparator and note block setup after placing, double slabs and stacks, chests, falling blocks. The camera never moves.
Usage: put schematics into `schematics/` next to `options.txt` of your instance (`/hm folder` shows the path), then `/hm load <name>` or press **M**. All commands and keys are in the [README](https://github.com/keksi2k/horizonmatica#usage).
## What it does and doesn't do
- **Reads** only files in the instance's `schematics/` folder, and creates that folder if it's missing.
- **Writes** nothing itself: settings and remembered placements go through Horizon's mod settings.
- **Network:** no connections of its own. The only thing sent to a server is what the printer does: the same block-placing clicks a player would make (with the matching look direction), and hotbar slot changes.
- **"Open folder"** in the menu opens the schematics folder in your file manager through Horizon's own function.
- No telemetry, no downloads, no code run from schematic files. Schematics are parsed as data only.
⚠️ On other people's servers, automatic placing may break the rules or trip anti-cheat. The printer is off until you turn it on (`K` or `/hm printer on`). Its *air place* option (on by default) is noticed by anti-cheat plugins such as Grim.
**Version:** native mods use game internals, so Horizonmatica is pinned to one Horizon commit (currently Nightly 2026-11-01, `4aa6854`). A new Nightly needs a new release.
## Credits
The idea, the way it's used, the file format and the behaviour come from **[Litematica](https://github.com/maruohon/litematica)** by masa (maruohon), maintained by **[Sakura-Ryoko](https://github.com/sakura-ryoko/litematica)**, and from **[Litematica Printer](https://github.com/aleksilassila/litematica-printer)** by aleksilassila. Horizonmatica is not an official project of these authors, isn't made or endorsed by them, and contains none of their code. Only the file format and the behaviour are re-created. If you like it, go and support the originals.
Menus built with [horizon-menus](https://github.com/keksi2k/horizon-menus). MIT licensed."""
authors = ["keksi2k"]
owners = ["keksi2k"]
license = "MIT"
source = "https://github.com/keksi2k/horizonmatica"
icon = "https://raw.githubusercontent.com/keksi2k/horizonmatica/main/icon.png"
key = "RWS+BRoCJslNCFapck3cbG2VFQcjlr1MUH5m3+CWA8zhJecoBoja7X94"
[[versions]]
version = "0.1.1"
published = "2026-11-02"
game = "26.3"
file = "horizonmatica-0.1.1.hnmod"
sha256 = "2ddc562dd2ea0f9a76a47eb761c64c803def68a8c81b92cb5d15ec26c5d432aa"
signature = """
untrusted comment: signature from minisign secret key\r
RUS+BRoCJslNCOkWo88+wHXYmMr1C6TzXpxZIGwzFuUN+K4lbd9RT6tLC8sIQQ5uD3QZQZx2rawTHRJvUvRhCgUNDoqhIek18w4=\r
trusted comment: timestamp:1793599192\tfile:horizonmatica-0.1.1.hnmod\thashed\r
BmryY+OjbG0qZnnFappBEO2tR8zx+a5BmqBFA4AVytjqvLnuvDWYE0PUwZLLodqbN218NM0JluVGpstnI3DvBQ=="""
changelog = """
First release in the registry.
- Loads .litematic, .schem (Sponge v1–v3), .schematic (MCEdit, 1.12 ids) and structure .nbt; older files are upgraded with Horizon's data fixer
- Ghost overlay with real block models, see-through mode, verifier colours and layer modes
- Placements remembered per world and dimension; move mode, rotate, mirror
- Own menu screen (M) and a material list with item icons
- Printer: places missing blocks within reach, checked against the game's own placement logic; the camera never moves
Since 0.1.0 on GitHub: loading another schematic no longer shows the previous one's blocks.
Built for Horizon Nightly 2026-11-01 (c9d474d, hook API 80)."""
+23
View File
@@ -0,0 +1,23 @@
id = "inventory_profiles"
kind = "native-mod"
name = "Inventory Profiles"
summary = "R-key & UI sorting (Category, A-Z, Count, ID), Auto-Refill depleted stacks, Tool Break Protection, and Quick-Stack to chests."
description = "A 1:1 native Rust port of Inventory Profiles Next & Inventory Sorter for Horizon. Press R or click the UI sort buttons to merge partial stacks and sort chests or your inventory using 4 selectable modes: Category (Creative Tab), Alphabetical (A-Z), Stack Count (High-Low), or Item ID. Includes automatic hand stack refilling when placing blocks or consuming items, Tool Break Protection that swaps out low-durability tools before they break, and G-key Quick-Stacking to move matching items into open containers."
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/inventory-profiles"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "inventory_profiles-1.0.0.hnmod"
sha256 = "1e5d1c75d05230172820b5bc016d7d3ac792adf473de7dbfb84394d8b795a633"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTNzTcNlnhp0xG8ZqxNqwzpSnc+ghT4/m+aR0dga48gdD04aioDSIPK5DBUtd4i2Vf+e4r7LKX3zb6T90aDdYJjgI=\r
trusted comment: timestamp:1793443789\tfile:inventory_profiles-1.0.0.hnmod\thashed\r
RcTkkWUMpp4nRQ4uXRFCD4m67ir4J0y3lo2nhOVUgriLIMndinAsVjnNsrfAoJLvKN9DO5vT/xp2+oW1iW7xAQ=="""
changelog = "Initial release for 26.3 Horizon. "
+39
View File
@@ -0,0 +1,39 @@
id = "inventory_wizard"
kind = "native-mod"
name = "InventoryWizard"
summary = "InventoryWizard's one-click sorting as a native mod for Horizon"
description = """
## Features
- **Smart sorting** by category (stone, earth, wood, ores, blocks, tools, weapons, armor, food, redstone,
transportation, decoration), from item tags and components, so other mods' items are sorted too
- **Survival-friendly hotbar**: weapons, tools, food, building blocks, utility items, then the rest
- **Stacks merged**, but enchanted or named items are never combined
- **Three modes**: Default, Alphabetical and Stack-based
- **Sort buttons** by the inventory and container titles, plus a mode button (it shows the mode's letter)
- **Locked slots**: **Alt+Click** a hotbar or inventory slot to lock it, and sorting leaves it alone
- **Keys**: **R** (or **Shift+Right Click**) sorts the part under the cursor: the hotbar sorts everything, the
inventory the inventory, a container the container. Outside a screen, R sorts the whole inventory.
**Shift+Right Click** on the middle hotbar slot cycles the mode. The mode also has its own key, unbound by default.
- **Commands**: `/iwiz [hotbar|inventory|all|mode]` (also `/inventorywizard`)
- **Works on** the inventory, hotbar, chests, barrels, shulker boxes and ender chests, **in singleplayer and on any
server**: every sort is made of ordinary left clicks, as a player could click them, so a vanilla server accepts it"""
authors = ["VanishingTacos"]
owners = ["VanishingTacos"]
license = "MIT"
source = "https://github.com/VanishingTacos/InventoryWizard-Horizon"
icon = "https://cdn.modrinth.com/data/cU0ynk3p/21b58402405493964af438e3e3b9551676b68433.png"
key = "RWTos7ZtiqwDTrtjtHk3WhMJJc1+sSTTyTu9CaDtIYWSQvww2D9WrAN1"
[[versions]]
version = "2.3.3"
published = "2026-11-01"
game = "26.3"
file = "inventory_wizard-2.3.3.hnmod"
sha256 = "03a7c8a528148611e5f309f3d05294a97b6766ca5e3bda9b3f1f79458fe65a42"
signature = """
untrusted comment: signature from minisign secret key\r
RUTos7ZtiqwDTmRp04qx/n5ZXozvaeOig3nJZFeNqmfmZ1PjB/2JUcfiF0JLZZJBkqpwm6sZQAvrt4HNce0juB5KaxJUOQJtAAo=\r
trusted comment: timestamp:1793498930\tfile:inventory_wizard-2.3.3.hnmod\thashed\r
Q0SE4CMnaFQHCmizFD0JcIPkUfiYQVs3KyGCCpS99m3OfFNhnS0gQt20ul0QlyYDAC8bAPJRjyrSIgcD1KzPAQ=="""
changelog = "Full native port of InventoryWizard for Horizon "
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+1 -1
View File
@@ -12,7 +12,7 @@ Horizon's own keystrokes overlay, formerly an extra in Options > Horizon Extras.
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
(install it on the launcher's Mods page, then press Build there). 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).
"""
+34
View File
@@ -0,0 +1,34 @@
id = "killc"
kind = "native-mod"
owners = ["Vakarux12"]
key = "RWS5u/7lEyKbzVHC6uMJRlhM/N7vzKp5fZe2LCDUlHsDK4lvPupxJbqQ"
name = "KillC"
summary = "/killc, /killc <player> and /suicide, with an optional random respawn near (0, 0)."
description = """
A Horizon port of [adasjusk's KillC](https://github.com/adasjusk/KillC), the kill command plugin for Paper, Folia, Velocity and Fabric.
- `/killc` and `/suicide` kill you.
- `/killc <player>` kills another player (operator level 2).
- After a death in the Overworld, players respawn at a random surface spot within `randomSpawnRadius` of (0, 0).
- Config: `config/killc.json` (`enabled`, `useRandomSpawn`, `randomSpawnRadius`), the same file as the Fabric build.
Vanilla keeps `/kill`. Runs where the world is hosted: single player and worlds opened to LAN (Horizon's dedicated server cannot build native mods yet). Native mods run with the player's account permissions and are not sandboxed.
Source: `horizon/` in https://github.com/Vakarux12/KillC, a fork of the original. CC-BY-SA-4.0, the original's licence, with a grant for Horizon's reviewers in `horizon/REVIEW-GRANT.md`.
"""
authors = ["adasjusk", "Vakarux12"]
license = "CC-BY-SA-4.0"
source = "https://github.com/Vakarux12/KillC/tree/main/horizon"
[[versions]]
version = "5.1.0"
published = "2026-11-01"
game = "26.3"
file = "killc-5.1.0.hnmod"
sha256 = "6c44d0a5e93aef42f2a33df8286980d9957a04526ff2c1884dfe7aa83f837086"
signature = """
untrusted comment: signature from minisign secret key
RUS5u/7lEyKbzbHxeuREFeXgBYM6FjB/wiwBtmdu5na35k0DXINdt3FILWvtW7yDekrFYjn4Xrl5pv9ttijPbocgpNbqZpPqLgY=
trusted comment: timestamp:1793694719 file:killc-5.1.0.hnmod hashed
vC2AGwA5JYAw1yjFLScjYyy+ef03gA5lxE3mwpD/D6CqKRblrpIKtHacgjYViC714ob5w1fgXFPya8AobYmVBg=="""
changelog = "First Horizon release, ported from adasjusk's KillC."
+23
View File
@@ -0,0 +1,23 @@
id = "light_overlay"
kind = "native-mod"
name = "Light Overlay"
summary = "1:1 Light Overlay & MiniHUD (F7): visualizes mob-spawnable blocks with 3D Yellow (night spawn) and Red (always/cave spawn) crosses (X) or 3D light level numbers"
description = "A 1:1 native Rust port of Light Overlay and MiniHUD's F7 Light Level Overlay for Horizon. Press F7 during gameplay to scan nearby spawnable block surfaces and render depth-tested 3D markers directly on block tops. Red markers indicate blocks where hostile mobs can spawn at any time (caves/indoor darkness), Yellow markers indicate blocks where hostile mobs can spawn at night, and optional Green markers show safe blocks. Supports Crosses (X), 3D Numbers (0-15), or Both, with configurable scan radius (8 to 20 blocks) and Modern (== 0) or Legacy (<= 7) spawn thresholds."
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/light-overlay"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "light_overlay-1.0.0.hnmod"
sha256 = "b4aeb56b13d9c73df267a1dc7b1f588f4db39bfd55f7beaef97a014fffb5b076"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTNy4CWemZ33Twp2eJ5FybX79KHuEbkVXRfv1C8+lWasqO1vOdx2A+8oN9eTR9XIh+QKjjw6xwrkG1u2nDoYkk5ww=\r
trusted comment: timestamp:1793444149\tfile:light_overlay-1.0.0.hnmod\thashed\r
3mGbUejwekJoW98TqQVK9QKS0aXMamM9rfCVOEwot/Xq/ZbaT1kUdDJMo6mn0iJIeodvdAFd7TnDEoguj7jkCA=="""
changelog = "Initial release for 26.3 Horizon. "
+1 -1
View File
@@ -11,7 +11,7 @@ The block or entity under your crosshair at the top of the screen, drawn as a va
**Settings** (Options → Mods → LookAt): range, position (top, top left, top right), size, background opacity, item icon, where it comes from, position and distance, entities, health, harvest tool. Boxes keep clear of F3, boss bars, the minimap and effect icons.
**Now a native mod (0.2.1)**: the launcher builds it into the game (Settings → Game → Native mods (experimental), then Build). Native mods are not sandboxed and run with your account's permissions, so read the source before you build it. Tested against Horizon `142388e1c136e622652ec7505af15d8ad195e041` with Horizon Mixins `0d6b103c70633d8fd9443d8d0acd3aadaacedb06`; the launcher builds these exact pinned revisions. Settings from 0.1.0 carry over. 0.1.0 is the WebAssembly version."""
**Now a native mod (0.2.1)**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). Native mods are not sandboxed and run with your account's permissions, so read the source before you build it. Tested against Horizon `142388e1c136e622652ec7505af15d8ad195e041` with Horizon Mixins `0d6b103c70633d8fd9443d8d0acd3aadaacedb06`; the launcher builds these exact pinned revisions. Settings from 0.1.0 carry over."""
authors = ["d3grando"]
owners = ["d3grando"]
license = "MIT"
+38
View File
@@ -0,0 +1,38 @@
id = "menu_panorama"
kind = "resourcepack"
name = "Menu Panorama"
summary = "Put your own wallpaper behind the main menu in Horizon — Minecraft 26.3, rewritten in Rust."
description = """
Menu Panorama is a resource pack for **Horizon** — Minecraft: Java Edition 26.3, rewritten from scratch in **Rust**.
It replaces the six textures of the main-menu background with a picture you choose. It is a plain resource pack, not a Java mod: no mod loader, no Java, and no code from the pack is executed. Horizon loads it natively.
- **Any photo** in the `room` mode: the picture is shown whole on the four walls, with mirrored seams and soft colours above and below.
- **A 360° equirectangular panorama** in the `360` mode: the sphere is cut into six cube faces, so the view stays seamless when the camera turns.
- **Auto** picks the mode from the image's aspect ratio.
Build a pack in the browser with `panorama-maker.html`: drop an image, choose a face size, download the `.zip`. Everything runs locally; the picture never leaves the computer. `make_panorama.py` does the same from the command line.
The pack only changes `assets/minecraft/textures/gui/title/background/panorama_0..5.png`; gameplay, sounds and other textures are untouched. `pack.mcmeta` is made for Horizon 26.3 (resource format 97) and also declares formats 15–999, so the same file loads in Minecraft Java 1.21+. A demo pack is attached to every release.
MIT licensed. Source, the browser generator and the CLI are in the repository."""
authors = ["form1t"]
owners = ["form1t"]
license = "MIT"
source = "https://github.com/form1t/menu-panorama"
icon = "icons/menu_panorama.png"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "menu_panorama-1.0.0.zip"
sha256 = "e0403b003e6c93dec683037202cebb47880025cfe21e2d8fc7818321ee362401"
changelog = """
First release.
- Demo sunset pack: six 1024×1024 faces and a 128×128 icon.
- `panorama-maker.html`: browser generator with drag & drop, auto/room/360 modes and 512/1024/2048 face sizes.
- `make_panorama.py`: CLI with the same modes, preview output and `--description`.
- `pack.mcmeta` for Horizon 26.3: resource format 97, `pack_format` 34, formats 15–999.
- MIT licence."""
+1 -1
View File
@@ -12,7 +12,7 @@ Horizon's own minimap, formerly an extra in Options > Horizon Extras.
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
(install it on the launcher's Mods page, then press Build there). 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).
"""
+14 -34
View File
@@ -1,42 +1,22 @@
id = "mod_menu"
kind = "mod"
kind = "native-mod"
name = "Mod Menu"
summary = "Browse installed mods, including native mods, with versions, enabled states, filters and search."
summary = "ModMenu For Horizon"
description = """
An independent, sandboxed mod browser by levzzz5154.
Adds a Mods button next to Accessibility on the title screen. It opens a Mod Menu style screen that lists every mod you have installed, with its icon, name, version and description.
Press **M** while playing, run **`/modmenu`**, or open
**Options → Mods → Mod Menu → Settings**. Use `/modmenu text` to search names,
ids and mod types. Filter all/enabled/disabled mods, browse pages, select a row
for details, and refresh after changing mods in the launcher.
Lists installed sandboxed, content and native source mods in the current instance.
Enabled means the launcher's selected state, not proof a mod is running. This is
a read-only menu: it does not toggle other mods, run commands, or start native builds.
**Requires a launcher with the schema-1 Mod Menu inventory feed**
([Horizon #3112](https://github.com/horizon-rust/horizon/pull/3112)). On a launcher
without it, the menu shows an explicit update message. The launcher supplies display
metadata through this mod's own sandbox storage; the mod has no general filesystem
or network access. Direct external `--dev-mod` paths and runtime failure status
are not included in the feed.
Tested on Horizon nightly `83f15fa27` (Minecraft 26.3), mod API 0.1 / API level 0.
Other Horizon revisions have unverified support. This `.hmod` does not require Rust,
Git, Horizon Mixins or any native build to install.
Independently written, not a port of another project's Mod Menu. No Minecraft code
or artwork is included. MIT licensed; source, tests and feed specification are linked below.
"""
authors = ["levzzz5154"]
owners = ["levzzz5154"]
- Search by name, id or description, and filter by on, off, native or content mods.
- Pick a mod to see its details, turn it on or off, or open its own settings page.
- Open Mods Folder and Done at the bottom, as in the Java mod."""
authors = ["tariksabanuz"]
owners = ["tariksabanuz"]
license = "MIT"
source = "https://github.com/levzzz5154/horizon-mod-menu"
source = "https://github.com/tariksabanuz/ModMenuHorizon"
icon = "https://raw.githubusercontent.com/tariksabanuz/ModMenuHorizon/main/icon.png"
[[versions]]
version = "0.1.1"
published = "2026-10-28"
version = "0.3.0"
published = "2026-11-03"
game = "26.3"
file = "mod_menu-0.1.1.hmod"
sha256 = "3833b79bbb9607c5cab6dbdc4a6d3e31754c2ed183b1c6bf431f3615b93ef3b5"
changelog = "First release: installed mods, enabled states, native/content/sandboxed types, paging, filters, search and refresh. Requires the launcher inventory feed."
file = "mod_menu-0.3.0.hnmod"
sha256 = "7896b3b5ad727ef6e2bc56039796713809483fb157daa385f78ebd3353d653bd"
+22
View File
@@ -0,0 +1,22 @@
id = "motion_blur"
kind = "native-mod"
name = "Motion Blur"
summary = "Blurs what moves: mobs, players, items and vehicles along their own motion, with optional camera blur."
description = """
Moving mobs, players, items, minecarts and boats blur along their own motion. The camera's motion (turning, walking) can blur too, and is off by default.
Each frame the mod works out every pixel's motion since the last frame (the world's from the depth buffer and the camera, entities from last frame's vertices), finds the largest motion near each tile, and gathers along it with depth-aware weights (McGuire et al., "A Reconstruction Filter for Plausible Motion Blur", 2012).
Settings are in Options → Mods → Motion Blur: strength for moving things and the camera, shutter angle or a fixed exposure, the longest blur, quality, blurring yourself in third person, and a view that shows each pixel's motion."""
authors = ["levzzz5154"]
owners = ["levzzz5154"]
license = "MIT"
source = "https://github.com/levzzz5154/horizon-motion-blur"
[[versions]]
version = "0.1.0"
published = "2026-11-02"
game = "26.3"
file = "motion_blur-0.1.0.hnmod"
sha256 = "4cdaa6369fffea8116b013edfc618358ca989f48ed65c83306d2102f0e506c47"
changelog = "First release, built against Nightly 2026-11-01: motion blur for moving mobs, players, items and vehicles, with optional camera blur."
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+4
View File
@@ -0,0 +1,4 @@
id = "radiant"
kind = "shader"
repo = "expensiverest/radiant"
owners = ["expensiverest"]
+1 -1
View File
@@ -15,7 +15,7 @@ Horizon's own recipe viewer.
It reads the game's items and recipes, hears its own keys and your clicks on the panel, and places recipes or gives items only when you ask: 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). It needs Horizon's native hook points version 2, in the
(install it on the launcher's Mods page, then press Build there). It needs Horizon's native hook points version 2, in the
Nightlies from 4baa571 on. Your settings and keys 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/recipes](https://github.com/horizon-rust/horizon-mods/tree/main/native/recipes).
+24
View File
@@ -0,0 +1,24 @@
id = "resourcify"
kind = "native-mod"
name = "Resourcify"
summary = "Download resource packs in Horizon"
description = """
Resourcify lets you browse and download resource packs without ever leaving Horizon.
Features:
- Browse resource packs right inside the game
- One-click download and install
- Search to find the pack you want quickly
- Native Horizon mod, fast and lightweight"""
authors = ["tariksabanuz"]
owners = ["tariksabanuz"]
license = "MIT"
source = "https://github.com/tariksabanuz/ResourcifyHorizon"
icon = "https://github.com/tariksabanuz/ResourcifyHorizon/blob/364648324ac0e67489a3f06faa716d96d24668ed/icon.png"
[[versions]]
version = "0.1.0"
published = "2026-11-03"
game = "26.3"
file = "resourcify-0.1.0.hnmod"
sha256 = "789447fb454864af2a711ad8204f395331248d70dba570016b248766b959a2a2"
+50
View File
@@ -0,0 +1,50 @@
id = "right_click_crops"
kind = "native-mod"
name = "Right-Click Crops"
summary = "Right-click harvest for 16 plant families, with replanting and individual settings."
description = """
Harvest mature plants with a right-click, replant crops, and keep your garden growing.
### Features
- Supports 16 plant families.
- Replants crops using one planting item from the harvest, or from your inventory.
- Keeps the base of tall plants and the stems of pumpkins and melons.
- Lets you enable or disable each plant separately in **Options > Mods > Right-Click Crops > Settings**.
### Supported plants
Wheat, carrots, potatoes, beetroot, nether wart, cocoa, torchflower, pitcher crop, sugar cane, cactus, bamboo, kelp, twisting vines, weeping vines, pumpkin and melon.
### Good to know
- Harvesting works in survival, one targeted plant at a time.
- Without a suitable planting item, the crop is harvested but not replanted.
- Torchflower needs spare seeds; pitcher crop needs spare pitcher pods.
- Twisting and weeping vines can drop nothing when harvested by hand. Use shears for guaranteed drops.
- Bone meal, ordinary breaking and planting, sweet berries and glow berries keep their normal behaviour.
### Compatibility
Last checked on **November 1, 2026**, for Minecraft 26.3. The release is pinned to Horizon Nightly 2026-10-31 (`9d979ca`), where single-player and LAN were tested. A compilation-only check also passed on the newest Nightly available on that date (`d91cab6`); gameplay on that newer revision was not tested.
This is a native mod: install only if you trust its source and accept the launcher's native-code warning.
[Watch the demo](https://youtu.be/kll-qvj5s54) · [Harvesting guide](https://github.com/AyanoKaito/horizon-right-click-crops/blob/main/docs/harvesting.md)
"""
authors = ["AyanoKaito"]
owners = ["AyanoKaito"]
license = "MIT"
source = "https://github.com/AyanoKaito/horizon-right-click-crops"
icon = "icons/right_click_crops.png"
key = "RWRwbuaByP5mM4VUm4kZEQVPiuw+3IEVJ7CZcEHIjY5fPKf9lhQKUZRy"
[[versions]]
version = "1.0.0"
published = "2026-11-01"
game = "26.3"
file = "right_click_crops-1.0.0.hnmod"
sha256 = "681ce896c50dd041132900e7bbab71167f133b3f3862f86e8b1a0cac52754b97"
signature = """
untrusted comment: signature from minisign secret key
RURwbuaByP5mM4x8HGWHv/S/WlHPsmamRwsi70PTq7E5u4uJ7wyNzNT4rwpR/vGA9V2H6w9yLhmF1CPtS1grEfWorPixnk6efAw=
trusted comment: timestamp:1793523764\tfile:right_click_crops-1.0.0.hnmod\thashed
seiWz0D/X34FIKhiE1yfZc2uAkkSGuwAnTHbGrK63sHg+HdL+EgqEkzaxNgJTiMOdnf/iIdSkDVVwuF+Od/SCQ==
"""
changelog = "First public release: right-click harvesting for 16 plant families, ordinary Horizon loot, paid replanting, retained bases and stems, and individual plant settings. Validated in single-player and for LAN host and guest."
+9 -4
View File
@@ -2,16 +2,21 @@ id = "rightharvest"
kind = "native-mod"
name = "RightClickHarvest"
summary = "Right-click a ripe crop to pick it and sow it again."
description = """
With an empty hand, right-click a ripe wheat, carrot, potato, beetroot or nether wart: the crop is harvested with the game's own drops, sound and statistics, and sown again from a seed in your inventory.
No settings: turn it off in Options > Mods."""
description = "With an empty hand, right-click a ripe wheat, carrot, potato, beetroot or nether wart: the crop is harvested with the game's own drops, sound and statistics, and sown again from a seed in your inventory."
authors = ["Domanffe"]
owners = ["Domanffe"]
license = "MIT"
source = "https://github.com/Domanffe/rightharvest"
icon = "https://horizon-rust.github.io/horizon-mods/icons/rightharvest.png"
[[versions]]
version = "0.1.1"
published = "2026-11-02"
game = "26.3"
file = "rightharvest-0.1.1.hnmod"
sha256 = "2f70f3a5abaca35f1ff8df7b198f0fd616eacaa5229f5113f75935e16f50255b"
changelog = "Rebuilt on the 2026-11-01 Nightly (Horizon `4aa6854`); the `mods` hook is named instead of pinned to a source target, so it survives an update that moves that point."
[[versions]]
version = "0.1.0"
published = "2026-10-30"
+34
View File
@@ -0,0 +1,34 @@
id = "rmotd"
kind = "native-mod"
owners = ["Vakarux12"]
key = "RWS5u/7lEyKbzVHC6uMJRlhM/N7vzKp5fZe2LCDUlHsDK4lvPupxJbqQ"
name = "RMotd"
summary = "Rotating server list MOTDs, managed in game with /randommotd and /motd."
description = """
A Horizon port of [adasjusk's RMotd](https://github.com/adasjusk/RMotd), the rotating MOTD plugin.
- The MOTD in the server list changes every 10 seconds, through the list in `config/rmotd.json` (the Fabric build's file).
- `/randommotd` turns rotation on and off.
- `/motd create <name> <motd>` adds an MOTD, `/motd reload` reads the file again (operator level 2).
- MOTDs take `&` colour codes.
It sets the MOTD of a world opened to LAN. The server list ping shows the rotation, the LAN broadcast keeps the first one. Horizon has no MOTD hook point, so a support module in `horizon_client::lan` writes the private MOTD field of the LAN server. Its injections are all hook points, but that field is not a stable API, so a later Nightly can break the build. Runs where the world is hosted: single player and worlds opened to LAN (Horizon's dedicated server cannot build native mods yet). Native mods run with the player's account permissions and are not sandboxed.
Source: `horizon/` in https://github.com/Vakarux12/RMotd, a fork of the original. CC-BY-SA-4.0, the original's licence, with a grant for Horizon's reviewers in `horizon/REVIEW-GRANT.md`.
"""
authors = ["adasjusk", "Vakarux12"]
license = "CC-BY-SA-4.0"
source = "https://github.com/Vakarux12/RMotd/tree/main/horizon"
[[versions]]
version = "1.0.0"
published = "2026-11-01"
game = "26.3"
file = "rmotd-1.0.0.hnmod"
sha256 = "68d43b760e95178a67bc1c7d54fbde004eee4bb764d9891d83bd4e4fa4bd8ce5"
signature = """
untrusted comment: signature from minisign secret key
RUS5u/7lEyKbzSnV+LLOgmX/1ryZ/5d3asf4KyfdUUOJKALk9isprehFuFIDN5X+GZy0C7jzHAImFnq9FRt2yc1J3N5lbA1oUgM=
trusted comment: timestamp:1793694734 file:rmotd-1.0.0.hnmod hashed
x+Ecd8KV0OFBcUEONAvAHxw87IG2MnjWZzcrIiMLjRNtl7KcuZQXvakrAm6slC1qADzj+n+HpwYoGT1QmQ07Bg=="""
changelog = "First Horizon release, ported from adasjusk's RMotd."
+1 -1
View File
@@ -33,7 +33,7 @@ Horizon cannot join servers that check Minecraft accounts yet, which is most pub
## Install
Install it from the launcher's Mods page, then Settings > Native mods (experimental) > Build, and play the Native mods instance. Guide: https://github.com/Louchatfroff/serverify/tree/main/horizon/serverify#readme
Install it from the launcher's Mods page, then press Build there, and play that instance. Guide: https://github.com/Louchatfroff/serverify/tree/main/horizon/serverify#readme
Native mods are not sandboxed. The source is public and licensed GPL-3.0-only: https://github.com/Louchatfroff/serverify/tree/main/horizon/serverify"""
authors = ["Louchatfroff"]
+1 -1
View File
@@ -3,7 +3,7 @@ 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.
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). 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.
+23
View File
@@ -0,0 +1,23 @@
id = "status_effect_hud"
kind = "native-mod"
name = "Status Effect HUD"
summary = "1:1 Status Effect Bars & Inventory HUD+ Potion HUD: displays active potion/beacon effects with icons, Roman numeral amplifiers, color-coded mm:ss countdown time"
description = "A 1:1 native Rust port of Status Effect Bars and Inventory HUD+ (Potion HUD) for Horizon. Renders all active potion and beacon status effects directly on the HUD during gameplay with their official mob_effect sprites, translated effect names, and Roman numeral potency levels (II, III, IV). Includes color-coded mm:ss countdown timers (White for >30s, Yellow for 10-30s, Flashing Red for <=10s urgent expiration warning, and Aqua infinity symbol for permanent effects) alongside smooth duration progress bars colored in each effect's authentic vanilla potion color."
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/status-effect-hud"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "status_effect_hud-1.0.0.hnmod"
sha256 = "afa30125bb8b402892f1719e550b66d4eb4d7264895f71bb9c98a4caf5c7b432"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTNzV+wcfmnupP9b3ECNs7/D2jW8vAV9eAd1rxLpETdSVxvuPpnbTPB/W9APouT8uLbsZLrB9wFsOoBtrhuUPmCw0=\r
trusted comment: timestamp:1793443789\tfile:status_effect_hud-1.0.0.hnmod\thashed\r
MkNOcfvQQP8dWwvaSdoGIasz6tYOCB13U0OVFIApxz9VzhCUGRZXTYaSitlSSoBfGqYCQk87zvliwvklR0lbDQ=="""
changelog = "Initial release for 26.3 Horizon. "
+35
View File
@@ -0,0 +1,35 @@
id = "stuck"
kind = "native-mod"
owners = ["Vakarux12"]
key = "RWS5u/7lEyKbzVHC6uMJRlhM/N7vzKp5fZe2LCDUlHsDK4lvPupxJbqQ"
name = "Stuck?"
summary = "/stuck and /escape move you to a safe spot nearby, off the Nether roof too."
description = """
A Horizon port of [adasjusk's Stuck?](https://github.com/adasjusk/Stuck), the unstuck plugin for Spigot, Paper, Folia and Fabric.
- `/stuck` and `/escape` search random columns near you for solid ground with two blocks of air above, and teleport you there.
- Skips lava, fire, magma, cactus and other hazards, and leaves and snow.
- A player on the Nether's bedrock roof is brought back under it.
- Cooldown between uses, 30 seconds by default.
- Config: `config/stuck.properties`, the same keys as the Fabric build.
Only loaded chunks are searched. Runs where the world is hosted: single player and worlds opened to LAN (Horizon's dedicated server cannot build native mods yet). Native mods run with the player's account permissions and are not sandboxed.
Source: `horizon/` in https://github.com/Vakarux12/Stuck, a fork of the original. CC-BY-SA-4.0, the original's licence, with a grant for Horizon's reviewers in `horizon/REVIEW-GRANT.md`.
"""
authors = ["adasjusk", "Vakarux12"]
license = "CC-BY-SA-4.0"
source = "https://github.com/Vakarux12/Stuck/tree/main/horizon"
[[versions]]
version = "1.0.0"
published = "2026-11-01"
game = "26.3"
file = "stuck-1.0.0.hnmod"
sha256 = "8e6292d9c89f660e72f01e54be4103b5273017955c7bd8034dd5ae8ec2e98630"
signature = """
untrusted comment: signature from minisign secret key
RUS5u/7lEyKbzf0/fn5Is2b8jJq55073yA8Sl7hSeLN1jAg9itcMlCO2kcEMVJaGyYrxFZIwPYfQJMCb61o/0g8XD8iYIUCeswU=
trusted comment: timestamp:1793694727 file:stuck-1.0.0.hnmod hashed
/JDVcy8eoaPHppahV7p+f+3LTx0CtfE34jqqZq7bU4TNvnSOx7ISCgp6Eg3+X1xYMRJRwTYM2aJwgbytRGzOCA=="""
changelog = "First Horizon release, ported from adasjusk's Stuck."
+23
View File
@@ -0,0 +1,23 @@
id = "toggle_sprint"
kind = "native-mod"
name = "Toggle Sprint"
summary = "1:1 Better Sprinting & Lunar ToggleSprint HUD: toggle sprint and toggle sneak with [Sprinting (Toggled)] / [Key Held] / [Flying] status HUD and configurable Cre"
description = "A 1:1 native Rust port of Better Sprinting and the Lunar/Badlion ToggleSprint HUD for Horizon. Allows toggling continuous sprint (V or vanilla Sprint key) and sneak (C) without holding keys down, while automatically pausing toggled sneak inside inventory and chest screens so Shift-clicking items works normally. Renders the classic movement status HUD ([Sprinting (Toggled)], [Sprinting (Key Held)], [Sneaking (Toggled)], [Sneaking (Key Held)], [Flying (2.0x Boost)], [Swimming], [Gliding (Elytra)], [Riding]) and includes a configurable Creative Fly Speed Boost multiplier (1.0x to 4.0x)."
authors = ["CayMac"]
owners = ["CayMac97"]
license = "MIT"
source = "https://github.com/CayMac97/toggle-sprint"
key = "RWS411U2KXdTN4/y5bQhqBYxNlZvCtSZ5lNy46eeSAcgy0cxSJo2+Sm3"
[[versions]]
version = "1.0.0"
published = "2026-10-31"
game = "26.3"
file = "toggle_sprint-1.0.0.hnmod"
sha256 = "b2a4190f9c19adbceae4a639342b7630bfc3047339eabf57673b7807db5edc6a"
signature = """
untrusted comment: signature from minisign secret key\r
RUS411U2KXdTN3mUUTEw+mmiuQhcddYpNrjRnGA43/GS3y/qgY1EnL0kGp4NTBouTlVJbiThEkQfOrqIjnwlshUB18PrQloxAAw=\r
trusted comment: timestamp:1793443789\tfile:toggle_sprint-1.0.0.hnmod\thashed\r
2kNF92yIivWMWhgUH7ypwU3pciKG3ouiHhpZrQmX8fWZHay2da9HgdOYFAUp5KaHJ1NAgBuwL1zMfwVf61X+Cw=="""
changelog = "Initial release for 26.3 Horizon. "
+13 -6
View File
@@ -7,16 +7,15 @@ An unofficial Rust port by levzzz5154. Mines connected veins, including diagonal
through Horizon's player-mining code, preserving loot, XP, tool wear and server block events.
Includes per-player toggles, configurable block/tool groups, a block limit and cooldown.
Native source mod: enable Settings → Game → Native mods (experimental), choose Build,
Native source mod: install it on the launcher's Mods page, press Build there,
and confirm the native-code warning. A supported launcher downloads its own Rust toolchain
and builds a separate game using Horizon Mixins. Requires Git and Horizon's platform build
dependencies, and runs with your account's permissions.
Tested against Horizon nightly `83f15fa276c44711276b6ceb5f0fe0c9c534dc66`
(`nightly/build-2026-10-28-3`) with Horizon Mixins 0.5.0 at
`0500ff24aa5933fca95a877ef41125353c4b4fe3`. The launcher builds these exact pinned revisions.
Other Horizon revisions have not been validated. Ordinary Play does not compile mods;
rebuild after changing the enabled native-mod combination.
Each version is pinned to one Horizon Nightly and Horizon Mixins revision, named in its
changelog; the launcher builds those exact revisions. Other Horizon revisions have not
been validated. Ordinary Play does not compile mods; rebuild after changing the enabled
native-mod combination.
Use `/veinminer toggle`, `/veinminer on`, `/veinminer off` or `/veinminer status`.
`/veinminer reload` requires operator permission. Survival mining only; install on the
@@ -37,3 +36,11 @@ game = "26.3"
file = "veinminer-0.3.0.hnmod"
sha256 = "ec308c02c65bad64043828a19ef6254d1eb9d422bb1123cd065cb9e6e7b126c8"
changelog = "Native launcher bundle, pinned to nightly 83f15fa27 and Horizon Mixins 0.5.0."
[[versions]]
version = "0.3.2"
published = "2026-10-31"
game = "26.3"
file = "veinminer-0.3.2.hnmod"
sha256 = "31a4249da1b35972ae8b34201373c86352f4adc2f5cb3e015749d1fbfecf00a0"
changelog = "Pinned to Nightly 2026-10-31 (d91cab6) and Horizon Mixins 4342a0b. Reads each level's height from the loaded world, so data-pack dimension heights are respected."
+42
View File
@@ -0,0 +1,42 @@
id = "voicechat"
kind = "native-mod"
name = "Horizon Voice Chat"
summary = "Talk with Simple Voice Chat players: proximity voice, groups, push to talk and a menu (V)."
description = """
An unofficial client for the [Simple Voice Chat](https://github.com/henkelmax/simple-voice-chat) protocol. On a server with the Simple Voice Chat mod or plugin, you can talk with players who use the Java mod on the regular launcher.
## What it does
- **Menu (V):** connection status, microphone and voice chat on/off, push to talk or voice activation, voice volume, microphone gain and activation level with a live meter, and every hotkey as it is bound now. The right side lists the server's groups with their members: join (with a password if needed), leave, or create a group.
- **Talking:** hold Caps Lock (push to talk) or use voice activation. Voices come from where players stand, getting quieter with distance and panned left or right.
- **Groups:** each player's group shows above their head. `/vc join <group> [password]`, `/vc leave`, `/vc create <name> [password] [normal|open|isolated]` and `/vc groups` work too.
- **Keys** (Options > Mods > Horizon Voice Chat): menu (V), push to talk (Caps Lock), whisper (unbound), mute (J), voice chat off/on (K).
- **Protocols:** Simple Voice Chat 2.6.x (compatibility 20) and 2.7 (21).
## What it touches
- It reads the server's `voicechat:*` custom payloads and other players' positions inside `game::remote`, so it is pinned to Horizon `c9d474d`.
- It opens one UDP connection to the server's voice chat port, encrypted with AES-128-GCM using the secret the server sends.
- It uses the default microphone and speakers through cpal, and loads the system **libopus** at run time (the `opus` package must be installed).
- It keeps its settings in Horizon's own mod settings. It contacts no host other than the Minecraft server you are on (and DNS, for the server's SRV record).
## Not affiliated
This project is not made, endorsed or supported by Max Henkel or the Simple Voice Chat project. It contains none of Simple Voice Chat's code: everything is written from scratch in Rust.
## Tested
The engine's unit tests pass, and the modded client type-checks. Live, against Paper 26.3 with the Simple Voice Chat 2.6.27 plugin, it authenticated and connected, a test tone was heard through the server, and groups and labels came from the server. Not tested yet: the menu in the real game, a real microphone, 2.7 servers, and talking with the Java mod itself.
"""
authors = ["Nirokx"]
owners = ["Nirokx"]
license = "MIT"
source = "https://github.com/Nirokx/horizon-voice-chat"
[[versions]]
version = "1.0.0"
published = "2026-11-02"
game = "26.3"
file = "voicechat-1.0.0.hnmod"
sha256 = "7a12ef9e691d10128ea081906a313d597c67f22897380618871477f9dd8eabf1"
changelog = "First release."
+31
View File
@@ -0,0 +1,31 @@
id = "waymap"
kind = "native-mod"
name = "Waymap"
summary = "Native Horizon waypoint mod with per-context storage, minimap dots, world markers, and full command suite."
description = """
Waymap — Native client-side waypoints for Horizon.
Features:
- World markers (3D, camera-projected)
- Minimap dots (rotation-aware)
- Waypoint list panel (bottom-left, 6 nearest)
- Per-context storage (per server server_<host:port>.txt / world world_<folder>.txt)
- Atomic saves (temp + atomic rename), legacy migration
- Commands: /wp add|del|list|rename|color|clear|edit|help
- Keys: N = add, B = toggle list"""
authors = ["Aron"]
owners = ["Lauch2"]
license = "MIT"
source = "https://github.com/Lauch2/horizon-waymap-mod"
key = "RWS1B1QZPeYaSNKJr9rffnfHDSvB0AZj/Ul1B2pKrJ2XFBgKXL0FC96u"
[[versions]]
version = "0.1.4"
published = "2026-11-02"
game = "26.3"
file = "waymap-0.1.4.hnmod"
sha256 = "61bf573af5b2711453ca37a41b4ac2abc78ab827863bcafb9bfabe7dbf19ccd5"
signature = """
untrusted comment: signature from minisign secret key
RUS1B1QZPeYaSBV67KshMqdN7e9tTtnd0aJ2j2hE0IOvZNQpo896FjvOETBrxu0OhB738CE44moftY8pvDMvF8o+dxXsfxQduAU=
trusted comment: timestamp:1793662341\tfile:waymap-0.1.4.hnmod\thashed
j0dStr7Yhq33q3uA+wVGhfiEfjdNx0XBCUIHiZ5AfD7vPhcCqtVnrnYx8KKcliC2UOMNBP0Kl3gOz2MyiqJSAQ=="""
+27
View File
@@ -0,0 +1,27 @@
id = "waypoints"
kind = "native-mod"
name = "Waypoints"
summary = "Place waypoints with N, manage them with M, and see their name and distance in the world."
description = """
Waypoints for Horizon.
- **N**: create a waypoint where you stand: name, X/Y/Z, dimension, colour, visible or hidden. Enter saves, Tab moves between fields, Esc cancels.
- **M**: the waypoint list, filtered by All / Overworld / Nether / End. Each row shows its colour, name, coordinates, dimension and distance, with Hide/Show, Edit and Delete buttons.
- **In the world**: each visible waypoint in your current dimension shows its name, its distance and a marker in its colour.
- **Options > Mods > Waypoints**: rebind both keys and turn the in-world labels on or off.
**What it does and doesn't do:** it only reads the game's own state (player position, dimension, camera, world folder name, server address) and draws on the HUD and its own screen. It stores waypoints as a text file per world and per server in `horizon-mods/.horizon-mod-data/waypoints/` inside the game folder. It uses only the Rust standard library, opens no network connections, and reads or writes no other files.
Pinned to Horizon `c9d474d`, because it reads the world folder, server address and last frame's camera from the game itself."""
authors = ["Nirokx"]
owners = ["Nirokx"]
license = "MIT"
source = "https://github.com/Nirokx/horizon-waypoints"
[[versions]]
version = "1.0.0"
published = "2026-11-02"
game = "26.3"
file = "waypoints-1.0.0.hnmod"
sha256 = "9c0e670f1205c23fed0fe0b0eaf8f0b90a01f3cbb883a8eee445ebc03f4d90a4"
changelog = "First release for Horizon 26.3 (Nightly c9d474d)."
+19
View File
@@ -0,0 +1,19 @@
id = "worldedit"
kind = "native-mod"
name = "WorldEdit"
summary = "working port of java world edit"
description = """
//we for commands
working schematics."""
authors = ["_cykoo_"]
owners = ["XnotCykoX"]
license = "MIT"
source = "https://github.com/XnotCykoX/WorldEdit-Horizon"
[[versions]]
version = "1.2.1"
published = "2026-10-30"
game = "26.3"
file = "worldedit-1.2.1.hnmod"
sha256 = "b2bb3019ecd4d5392644c54c70a87d6c3277538a6e18d0c311f3a051f7d2f015"
+1 -1
View File
@@ -13,7 +13,7 @@ Horizon's own zoom, formerly an extra in Options > Horizon Extras.
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
(install it on the launcher's Mods page, then press Build there). 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).
"""
+3 -2
View File
@@ -6,12 +6,13 @@ Your block position and the direction you face in a box in the top-left corner o
- `/coords` hides or shows the box, and the game keeps your choice (also Options > Mods > Coordinates HUD > Settings).
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/coords_hud/`) that the Horizon
launcher builds into the game. It replaces the WebAssembly Coordinates HUD 0.1.0, the WebAssembly SDK's example mod.
launcher builds into the game. It is a small example mod.
The time of day line is gone: Horizon's hook points don't give the game's clock yet.
## Install
In the Horizon launcher: Mods → Coordinates HUD → Install, then Settings → Game → Native mods (experimental) → Build. The
In the Horizon launcher: Mods → Coordinates HUD → Install, then **Build** on the same page (once built, switch it
on or off in the game under Options → Mods). 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).
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "4baa571566e39bfd6594988d3bb2bc85440c30e7",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+4 -3
View File
@@ -3,11 +3,12 @@
Horizon's official FPS counter: frames per second in a box in a corner of the screen, and, if you like, the slowest frame and the 1% low of the last 5 seconds. Its settings page (Options > Mods > FPS Counter > Settings) picks the corner and the lows.
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/fps_overlay/`) that the Horizon
launcher builds into the game. It replaces the WebAssembly FPS Counter 1.x and keeps its settings: the same files (`horizon-mods/.horizon-mod-data/fps_overlay/corner`, `lows`).
launcher builds into the game. It keeps the settings of earlier versions: the same files (`horizon-mods/.horizon-mod-data/fps_overlay/corner`, `lows`).
## Install
In the Horizon launcher: Mods → FPS Counter → Install, then Settings → Game → Native mods (experimental) → Build. The
In the Horizon launcher: Mods → FPS Counter → Install, then **Build** on the same page (once built, switch it
on or off in the game under Options → Mods). 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).
@@ -38,6 +39,6 @@ python3 native/tools/native.py proof fps-overlay --resources /path/to/.local/hor
```
The proof needs resources prepared from your own Minecraft 26.3 (`horizon-prepare`); none are in this repository.
It seeds the WebAssembly mod's settings (bottom right, lows on) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the box to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
It seeds an earlier version's settings (bottom right, lows on) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the box to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
License: MIT.
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+4 -3
View File
@@ -3,11 +3,12 @@
Horizon's official Keystrokes: your movement keys, mouse buttons and space bar in a corner, lit while held, with clicks per second. Its settings page (Options > Mods > Keystrokes > Settings) picks the corner and the clicks per second.
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/keystrokes/`) that the Horizon
launcher builds into the game. It replaces the WebAssembly Keystrokes 1.x and keeps its settings: the same files (`horizon-mods/.horizon-mod-data/keystrokes/corner`, `cps`).
launcher builds into the game. It keeps the settings of earlier versions: the same files (`horizon-mods/.horizon-mod-data/keystrokes/corner`, `cps`).
## Install
In the Horizon launcher: Mods → Keystrokes → Install, then Settings → Game → Native mods (experimental) → Build. The
In the Horizon launcher: Mods → Keystrokes → Install, then **Build** on the same page (once built, switch it
on or off in the game under Options → Mods). 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).
@@ -38,6 +39,6 @@ python3 native/tools/native.py proof keystrokes --resources /path/to/.local/hori
```
The proof needs resources prepared from your own Minecraft 26.3 (`horizon-prepare`); none are in this repository.
It seeds the WebAssembly mod's settings (bottom right, no clicks per second) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the keys to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
It seeds an earlier version's settings (bottom right, no clicks per second) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the keys to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
License: MIT.
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+4 -3
View File
@@ -3,11 +3,12 @@
Horizon's official Minimap: a map of the terrain around you in a corner, in map colours, square or round, north up or turning with you, drawn by Horizon's own minimap. Its settings page (Options > Mods > Minimap > Settings) picks the corner, size, shape, zoom and turning.
It is a [horizon-mixins](https://github.com/horizon-rust/horizon-mixins) bundle (`mixins/minimap/`) that the Horizon
launcher builds into the game. It replaces the WebAssembly Minimap 1.x and keeps its settings: the same files (`horizon-mods/.horizon-mod-data/minimap/corner`, `size`, `shape`, `zoom`, `rotate`).
launcher builds into the game. It keeps the settings of earlier versions: the same files (`horizon-mods/.horizon-mod-data/minimap/corner`, `size`, `shape`, `zoom`, `rotate`).
## Install
In the Horizon launcher: Mods → Minimap → Install, then Settings → Game → Native mods (experimental) → Build. The
In the Horizon launcher: Mods → Minimap → Install, then **Build** on the same page (once built, switch it
on or off in the game under Options → Mods). 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).
@@ -38,6 +39,6 @@ python3 native/tools/native.py proof minimap --resources /path/to/.local/horizon
```
The proof needs resources prepared from your own Minecraft 26.3 (`horizon-prepare`); none are in this repository.
It seeds the WebAssembly mod's settings (bottom right, round, 2 blocks per pixel) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the map to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
It seeds an earlier version's settings (bottom right, round, 2 blocks per pixel) in a temporary game folder, then checks that Options > Mods shows them, and that the Corner button moves the map to the top left and writes the same file. CI runs `cargo test`, packages the `.hnmod` and checks the game on the tested commit, and every Monday on the newest Nightly.
License: MIT.
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "6f8c7a0e5d531d3834c4d0be4659a1db8e7e5c2a",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+4 -3
View File
@@ -7,13 +7,14 @@ Settings): a click on an item in the panel gives you one, a right click a stack.
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
launcher builds into the game. It keeps the settings of earlier versions: 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
In the Horizon launcher: Mods → Recipes → Install, then **Build** on the same page (once built, switch it
on or off in the game under Options → Mods). 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).
@@ -44,7 +45,7 @@ python3 native/tools/native.py proof recipes --resources /path/to/.local/horizon
```
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
It seeds an earlier version'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.
+1 -1
View File
@@ -4,5 +4,5 @@
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "4baa571566e39bfd6594988d3bb2bc85440c30e7",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+1 -1
View File
@@ -4,7 +4,7 @@ version = 4
[[package]]
name = "horizon-mod-smp"
version = "0.5.0"
version = "0.31.0"
dependencies = [
"serde",
"serde_json",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "horizon-mod-smp"
version = "0.5.0"
version = "0.31.0"
edition = "2024"
license = "MIT"
publish = false
+273 -13
View File
@@ -8,15 +8,26 @@ Players install nothing: it speaks only what a stock Horizon client already show
lines, vanilla chest menus the server fills with ordinary items (the trade menu); later sounds, titles, the action
bar, boss bars and the sidebar).
This version: the economy, the server market, player trades, the coinflip and the lottery.
This version: the economy, the auction house, the server market and its `/shop` menu, player trades, the coinflip, the lottery, places (spawn, homes, warps,
teleport requests, the rules), land claims, and the scoreboard (a sidebar with your balance and playtime, the Tab list's
header and footer).
| Command | Does |
|---|---|
| `/balance [player]`, `/bal` | your balance, or another player's |
| `/pay <player> <amount>` | gives money to a player who has an account (anyone who has played since the mod was on) |
| `/baltop [page]` | the richest players, 10 a page |
| `/eco give\|take\|set <player> <amount>` | operators of level 3: corrections, logged |
| `/worth [item]` | what the market pays and asks for an item (the one in your hand without a name) |
| `/eco give <player> <amount> <reason>`, `/money give ...` | operators of level 3: makes money for a player (an event prize, a bug refund); everyone reads it with the reason, and the audit log keeps it |
| `/eco take <player> <amount> [reason]`, `/money take ...` | operators of level 3: takes a cheat's gains out of the game, logged with the reason |
| `/eco stats` | the money supply and where it sits, what the server made and destroyed today, how evenly money is spread |
| `/ah` | the auction house: a chest menu of listings by category, cheapest a unit first; click one to buy it or bid; your listings (click to take one back) and your mail |
| `/ah sell <price> [count]` | lists the stack in your hand (or part of it), whole (enchantments, name, contents...), at a fixed price, for `listing_hours` (48); listing is free |
| `/ah auction <start> [hours]` | auctions the stack in your hand from a starting bid, for 1 to 24 hours (12) |
| `/ah bid <listing> <amount>`, `/ah buy <listing>` | bids (the money is held until someone bids more or it ends), buys a fixed-price listing |
| `/ah cancel <listing>`, `/ah claim`, `/ah search <words>`, `/ah mine`, `/ah mail` | takes back a listing nobody bid on; gives you your mail; the menu on a search, your listings, your mail |
| `/shop` | the market as a chest menu: categories, a click buys one, a shift-click 64, a click on a stack of yours sells it, "Sell all" |
| `/market [item]` | the moving market as a chest menu: categories, each item's price, its move over 1 h and 24 h and its volume; an item's page buys or sells 1, 16 or 64, sells all you carry, and gives its price chart as a map |
| `/worth [item]` | an item's price, the same to buy and to sell (the one in your hand without a name), and what you may still sell today |
| `/sell hand [count]`, `/sell <item> [count]`, `/sell all` | sells to the market: the stack in your hand, an item, or everything it buys |
| `/buy <item> [count]` | buys from the market, only what fits in your inventory |
| `/trade <player>` | asks a player to trade (60 s to answer) |
@@ -25,12 +36,131 @@ This version: the economy, the server market, player trades, the coinflip and th
| `/trade cancel` | ends your trade, or withdraws your request |
| `/coinflip <amount> [player]` | stakes an amount for anyone (or one player) to match; off unless `gambling.enabled` |
| `/lottery`, `/lottery buy <tickets>` | the pot, your tickets and the time to the draw; buys tickets (off unless `gambling.enabled`) |
| `/casino`, `/casino roulette\|slots\|crash\|blackjack` | the casino's lobby menu, or a game's table (needs `gambling.enabled` and `casino.enabled`) |
| `/casino help`, `/casino top`, `/casino stats` | every game's odds and house edge and the limits; today's biggest wins; what you bet, won and may still lose today |
| `/coinflip accept <player>`, `/coinflip cancel`, `/coinflip list` | takes an offer (a fair coin decides, the winner gets both stakes), withdraws yours, lists those waiting |
| `/spawn` | to a safe spot within `places.spawn_radius` of the server's spawn; `/setspawn` (operators of level 3) sets it where you stand |
| `/home [name]`, `/sethome [name]`, `/delhome [name]`, `/homes` | your homes (`home` when unnamed): go, set or move one where you stand, delete, list |
| `/warp <name>`, `/warps` | to a warp, the list; `/setwarp <name>`, `/delwarp <name>` for operators of level 3 |
| `/tpa <player>`, `/tpaccept [player]`, `/tpdeny [player]` | asks to go to a player (60 s to answer); answers a request |
| `/rules` | the server's rules |
| `/back` | back to where you died, or where your last teleport took you from (warmup and cooldown as any teleport) |
| `/afk` | marks you away from the keyboard until you move, turn or run a command |
| `/playtime [player]` | how long you (or a player who is on) have played here |
| `/claim`, `/claim info`, `/unclaim` | claims the chunk you stand in (all its height), says whose it is, frees it (operators of level 3: anyone's) |
| `/claim show` | draws the border of the chunk you stand in for 10 s, for you only: green yours or a trusting owner's, red someone else's, white free |
| `/claims` | your chunks, how many you may hold, and who you trust |
| `/trust <player>`, `/untrust <player>` | lets a player (or no longer) build and use things on all your land |
| `/nick <name>`, `/nick off`, `/nick` | operators of level 3 (with `ops_only`, the default): another name for you in chat and the Tab list, shown as `~name` (hovering it gives your real one); `/nick off` (anyone with a nickname) takes it off; `/nick` alone says how |
| `/nick <player> <name\|off>` | operators of level 3: sets or takes off a player's nickname |
| `/realname <nick>` | who goes by that nickname |
| `/invsee <player>`, `/endersee <player>` | operators of level 3: the player's inventory (main, hotbar, armour, off hand) or ender chest in a chest menu, on or off the server; click one of theirs to take it, one of yours to give it; every move in `audit.log`, a summary in `staff.log` |
| `/sidebar off`, `/sidebar on`, `/sidebar` | hides the scoreboard sidebar for you (remembered), shows it again |
The market trades only plain items (not worn, named or enchanted), and only with players in survival or adventure
mode. Items move first, then money, both logged; a sale the log refuses gives the items back, and a purchase checks the
money and the room before anything moves.
**No way to get rich from the market.** The market makes money only from what players gather, and only so much a day:
- One price per item, to buy and to sell (since 0.29): a config that still has two (`buy` 400, `sell` 100) gets
their geometric mean (200) at load, logged ("one price now"); `buy` or `sell` 0 still closes that side. A trade
of N items is priced along the curve it moves the price on (below), so buying N and selling them back gives back
exactly what was paid (less whole-coin rounding): a round trip never earns (tested for 1, 64 and past the minute's band, and
over thousands of random trades with the drift in between).
- No chain of recipes turns what the market sells into something it buys for more: the cheapest way to get each item
from the market (bought, or crafted, smelted, cut from bought items, through any number of recipes) is worked out,
and an item the market would buy for more than that isn't bought. The tests walk every vanilla recipe
(`SMP_VANILLA_DATA=<.local/horizon-client/26.3/data/minecraft> cargo test`); the server checks the iron, gold,
copper, diamond, emerald, coal, redstone, lapis, wheat, wood and food chains at each start.
- Items a recipe links (an ingot and its block, wheat and hay...) share one moving price, the block moving it as
its 9 ingots do, so crafting never beats the market however prices move; the vanilla-recipe test checks every item
at the worst drift (what makes it at 0.6 times its price, it at 1.6) too.
- Each player sells at most `daily_sell_coins` (2,000 coins) and `daily_sell_items` (640 of each item) a UTC day,
`/worth` and the shop show what is left, and `market.json` keeps the day's count across restarts.
- Money received with `/pay` counts toward the receiver's same `daily_sell_coins`: selling up to the limit on alts
and paying it all to a main is refused once the main's limit is reached ("can only receive N more today").
Trades (`/trade`) and auction house sales are not counted.
- Amounts are checked: no price above 1,000,000,000,000, no sum that overflows.
- The coinflip and the lottery give the winner all the stakes: they move money between players and never make any.
Default prices (coins; "buy only": the market sells it but doesn't buy it, "sell only" the other way):
| Category | Items |
|---|---|
| Farming | wheat 2, carrot 2, potato 2, beetroot 2, sugar cane 2, pumpkin 5, melon slice 1, cactus 2; buy only: wheat seeds 2, bone meal 3 |
| Mining | coal 4, copper ingot 3, iron ingot 15, gold ingot 20, redstone 2, lapis 4, quartz 4, emerald 40, diamond 200 |
| Wood | the eight logs 2 |
| Mob drops | bone 2, string 2, gunpowder 4, spider eye 2, slime ball 4, blaze rod 15, leather 3, feather 1; sell only: rotten flesh 1 |
| Building (buy only) | cobblestone 1, stone 2, dirt 1, sand 2, gravel 2, glass 3, bricks 4, stone bricks 3 |
| Food (buy only) | bread 5, cooked beef 6, cooked porkchop 6, cooked chicken 5, baked potato 4, apple 4, golden carrot 40 |
| Tools (buy only) | torch 1, ladder 2, crafting table 4, chest 6, furnace 6 |
A `config.json` still holding the 18 prices of versions 0.1 to 0.11, never edited, gets these at the first start of
0.12.0; edited prices are kept. Only items that stack to 64 belong in the market: a shift-click buys 64, and what
doesn't fit falls at the player's feet for them.
**/invsee and /endersee.** For operators of level 3 (luzW, bunchy7s...). The menu shows the main inventory in its
first three rows, the hotbar in the fourth, then the armour (head to feet) and the off hand; the ender chest fills
three rows. A click on one of the player's stacks moves it whole into the operator's inventory; a click on one of the
operator's own moves it into the player's (main inventory or ender chest, where it fits whole). A stack moves only if
it is still exactly what the menu showed: the game checks it and moves it, or nothing (no copy, no loss). A player who
isn't on is read from and written to their saved file; while they log in or are dead nothing moves and the menu says
why. The menu is read again once a second while open; nothing runs when none is open. Each move is in `audit.log`
before it is made (`invsee_take`, `invsee_put`: operator, player, inventory, item, count; a refused one again with
its reason); closing the menu writes one line to the server log (`[smp] staff: ...`), `staff.log`, and the chat of
the other operators on. Needs horizon#6106 (`native_inventory`, `native_move_stack`).
**Nicknames.** With `ops_only` (the default since 0.22) only operators of level 3 set nicknames: their own, or a
player's with `/nick <player> <name>`; a player may take off the one given to them (`/nick off`). At each start, a
nickname a player who isn't an operator picked themselves is taken off and logged (`for operators only`). 3 to 16 letters, digits or `_`, shown with a `~` in front (`~Rocky`) so a nickname never passes for
a real name. Names are compared case-insensitively with `_` dropped and look-alikes folded (`0`→o, `1`/`i`→l, `3`→e,
`4`→a, `5`→s, `7`→t, `8`→b, `9`→g). Refused: a nickname equal to, or containing (from 4 letters), the name of anyone
who has played here (the economy's accounts and `usercache.json`), an operator (`ops.json`), the Team (`protected`)
or another player's nickname; one that reads as a reserved word (`reserved`: server, console, admin, mod, owner,
staff, op, horizon, kira, notch, mojang, herobrine...); one the server's chat filter refuses. A player needs
`min_playtime_minutes` of play before their first nickname (operators don't). At each start, nicknames that break
these rules are taken off, each logged (`[smp] nickname X of <uuid> taken off: why`). The change is made at the next
second. The nickname shows in chat (messages, `/me`, `/msg`) and the Tab list, and hovering it gives the real name;
the real name stays everywhere else: the server's log, the anticheat, selectors, the Discord bridge. `/pay`, `/tpa`,
`/trade`, `/playtime`, `/tpaccept`, `/tpdeny` and `/eco` take a nickname (with or without `~`) wherever they take a
player's name. The player's profile (name, UUID) never changes.
`config.json`'s `nicks`: `enabled`, `ops_only` (true), `cooldown_seconds` (600), `min_playtime_minutes` (60), `protected` (the Team's
names), `reserved` (the words above).
**Moving prices** (`market.dynamic`, on by default since 0.26). Each item has one price: its base price (`items`)
times a factor that supply and demand move as each trade happens. Every item bought raises the log of the factor by
`1% / depth`, every item sold lowers it as much, where the depth follows the item's rarity (400 / its price, between
2 and 400: a diamond moves 50 times faster per item than cobblestone; `depth` overrides it per item). A trade of N is
worth the area under that curve (`price / step × (e^(N × step) - 1)`), rounded up to whole coins when buying and down
when selling: one diamond at 200 costs 201 and sells for 199, 64 cobblestone at 1 cost 65, and a sale walks back the
exact path of the purchase. All trades together move an item at most `max_minute_move_percent` (5%) a minute from
where it started that minute: past that, a purchase is cut short and a sale keeps the rest ("sell them in a
minute"). The factor goes back to 1 with a half-life of `half_life_hours` (6) and stays within `min_factor` (0.6)
and `max_factor` (1.6), where the rest of a trade goes at that price. There is no tax on trades (an older config's
`tax_percent` is ignored): the receipt is the price ("Sold 3 diamond at 198 coins each: 595 coins."). Items a player bought from the market in the last `round_trip_hours` (24) can't be
sold back to it. (`max_trade_impact_percent` and `max_player_impact_percent` are no longer used: the curve and the
minute's band do their work.) History: one point a minute for 24 hours and one an hour for 30 days (the mid price, the
items traded and those sold), in `market_prices.json`, written by a thread every 5 minutes and when the world closes.
At the first minute of each UTC day, the day's three biggest risers and fallers (at least 1%) and news from real
activity ("Diamond supply up 40% today", sales against the day before's, at least `news_percent` and
`news_min_volume`) go to everyone's chat and to `web/market.json` for Kira to post. `enabled: false` keeps the
fixed prices of 0.23. For other mods: `Market::spot`, `volume(id, now, hours)`, `history(id, Span)`.
**`/market`.** A 6-row chest menu: the shop's categories and the day's movers; a category's items, each with its
price, its change over 1 h and 24 h (green ▲, red ▼) and what was traded in 24 h; an item's page with its
base price and what you may still sell today, buttons to buy or sell 1, 16 and 64 (what they come to
along the curve), sell all of it you carry, and the price chart over 24 hours or 7 days, given as a filled map (128 × 128,
drawn by the server: the curve green when it rose, red when it fell, the price, the change, the low and high). A
chart map is drawn again each minute for whoever carries it, and once a minute the open menus follow the prices.
`/market <item>` opens the item's page. `/worth` adds the item's moves and volume.
**The shop.** `/shop` opens a 6-row chest menu on the market's prices: the categories of `config.json`'s `shop` (an
item in none goes under "Other"), each item named with its prices. A click buys one, a shift-click 64; a click on a
stack in your own inventory, below the menu, sells it; "Sell all" sells everything the market buys. The bottom row
shows your balance and today's selling limit. It goes through the market's own rules (survival or adventure only,
items then money, logged, the daily limits); the items move by the console's `clear` and `give`, measured on the
inventory before and after, so the money follows what really moved.
**Trades.** Once accepted, both players see a 6-row chest menu: their own offer on the left (16 stacks), the other's
on the right, each side's money, their confirm and cancel buttons, and whether the other has confirmed. A click on a
stack in your inventory, below the menu, adds it to your offer; a click on it in your offer takes it back. The menu
@@ -42,16 +172,129 @@ the trade, and swaps everything in one step, or nothing (and both must confirm a
adventure players only.
**Coinflips.** Off by default: the owner turns them on in `config.json` (`gambling`: `enabled`, `min_bet` 10,
`max_bet` 1000, `cooldown_seconds` 30, `offer_seconds` 60, `fee_percent` 0). A stake is held from the offer, in
`max_bet` 1000, `cooldown_seconds` 30, `offer_seconds` 60). A stake is held from the offer, in
`economy.json` with the balances, until the coin falls; an offer cancelled, not taken in time, or whose player
left gives it back, and so does the next start for one the server stopped with. The coin is a fresh random key from
the system, never the world's random. Only the in-game currency is staked and won.
**The lottery.** Part of `gambling` (`ticket_price` 10, `max_tickets` 100 a player a draw, `lottery_seconds` 3600
from a round's first ticket to its draw). Tickets are held like coinflip stakes; at the draw one ticket is picked at
random among all sold (more tickets, more chances), its owner gets the pot (less `fee_percent`), and everyone hears
random among all sold (more tickets, more chances), its owner gets the whole pot, and everyone hears
it. A round with a single player, or one the server stopped with, gives every ticket back.
**The casino.** On with `gambling.enabled` (and `casino.enabled`, true by default). Every game is a chest menu any
stock client can play, animated by changing its items at most 5 times a second for each open menu (and only while a
round moves), with note-block sounds, titles and the action bar for results, fireworks and a line for everyone on a
jackpot:
- European roulette: one zero, red/black, odd/even, 1-18/19-36 pay 1 to 1, dozens and columns 2 to 1, a number 35 to
1; house edge 2.70% on every bet. The wheel turns and slows down on the drawn pocket.
- Slots: 3 reels of 32 stops with item symbols, one payline, a published paytable (three Nether Stars 500x down to
three Berries 6x, two Berries 2x); 94.2% paid back (house edge 5.8%), one pull in 4.5 pays.
- Crash: one shared round; bet in the 10 s before take-off, then cash out (or set an automatic cash-out) before it
crashes. P(crash at x or more) = 96% / x, capped at 100x: 96% paid back whatever you do. Each player in the round
sees the multiplier in their own boss bar and the action bar.
- Blackjack: 6 decks shuffled each hand, the dealer stands on all 17s and peeks, blackjack pays 3 to 2, double on the
first two cards, no split; about 1% house edge with basic strategy.
Every draw is made by the server (ChaCha20 keyed from the system) before the animation starts. Bets go from `min_bet`
10 to `max_bet` 1000; a player may lose at most `daily_loss_cap` (10,000) a day (UTC, all games together) and win at most
`daily_win_cap` (10,000) net (once reached, no new bet until 00:00 UTC; the round that crosses it is paid in full), and
`cooldown_ms` (2000) passes between the end of a round and their next bet. A bet is held like a coinflip stake;
closing the menu or leaving never loses a round: a spin or a pull ends and is paid, a blackjack hand stands, a crash
bet is cashed out when its player leaves in flight (or given back before take-off). Wins are paid from the casino's
pot (`economy.json`'s pool `casino`): a win larger than the pot is paid up to it. A new casino's pot gets `float`
(20,000) once, made for it (`casino_float`; an older pot only what it lacks of it), then keeps every lost bet: the
house edge stays in the pot, out of the players' hands. A win of `jackpot_multiple` (50) times the stake, or of `announce_profit` (5000) coins, is
told to everyone. Settings in `config.json`'s `casino`, also `crash_edge_percent` (4) and `crash_max` (100).
**Places.** A teleport waits `warmup_seconds` (3) in `config.json`'s `places`; a player who moves or is hurt meanwhile
stays where they are, and `cooldown_seconds` (10) passes between two of one player's teleports. Teleports are free (an older config's
`places.fees` is ignored). `/tpa` takes the
asker to where the other player is when the warmup ends. Homes grow with the time played on the server since the mod
was on (`homes`: `[hours, homes]`, by default 1 home, 2 from 5 hours, 3 from 20, 5 from 100). `rules` holds `/rules`'
lines. `/spawn` lands on a random safe column within `spawn_radius` (250) blocks of the spawn point, like vanilla's `respawn_radius` for a first join or a respawn: after the `tp`, vanilla's `spreadplayers` moves the player off fluid, fire and magma onto solid ground (0: exactly the spawn point; if no column is found the player stays at the spawn point). Teleports are vanilla's `tp`, run as the server (`execute in <level> run tp`), so they cross levels and land
as any teleport does.
**Claims.** In a claimed chunk, only its owner, the players they trust and operators of level 3 break blocks or right
click blocks: doors, chests, levers, buttons, placing a block, a bucket or flint on a block. Anyone else's click does
nothing, and the action bar says whose land it is. The chunks a player may hold grow with the time played here
(`claims.chunks`: `[hours, chunks]`, by default 4, 16 from 5 hours, 36 from 20, 64 from 100); `claims.enabled`
turns them off. `claims.free_radius` (0, none) keeps the land within that many blocks of 0 0, in every dimension, free
for all: no one claims a chunk that touches it, and claims already there are dropped at start (each logged). The world stays out too: no water or lava flows, no fire spreads or burns, and no piston pushes into
a claim from land that isn't its owner's (or a player's they trust), and explosions break no claimed block, whoever
lit them. Buckets are guarded where they fill or empty, and hitting the claim's item frames, armour stands,
paintings, boats, minecarts and animals does nothing for others (`claims.protected_entities`, entity ids; monsters
and players aren't in it). Not guarded yet: right clicking an entity (rotating a frame's item, an armour stand's
items, shearing, leads), which waits for Horizon's hook (#4648). The server's `spawn-protection` (vanilla's) guards
the spawn.
**The scoreboard.** A sidebar on the right, titled `board.title` ("Horizon SMP"): your own block first (your name, or
your nickname; your balance, `125.4k`, `3.2M`; your playtime, `2h 05m`; the players online), then `board.footer`
(`play.playhorizon.dev`, empty for none). It lists no ranking: `/baltop` does (`board.top`, of 0.14 to 0.29, is
ignored). The balances are read once for everyone, only when one changed (at most every `board.min_seconds`, 5) or
every `board.refresh_seconds` (60); each player's own lines are looked at on the same `board.refresh_seconds`, and a
sidebar is sent only to the players whose lines changed and to newcomers: nothing runs per tick per player. The Tab
list shows `board.tab_header` and `board.tab_footer` (`{server}`, `{tps}`, `{players}` filled in, every
`board.tab_refresh_seconds`, 5). `board.sidebar` and `board.tab` turn either off; `board.server_name` names the Tab
list. A server that kept 0.13's title (`Richest players`) gets `Horizon SMP` at its first start. The
sidebar uses one scoreboard objective of its own (`horizon:native_sidebar`) and leaves the world's scoreboard alone.
**Presence.** A player's first join is announced to everyone (`presence.first_join`, empty for none) and they read
`presence.welcome` and the rules. Each player gets `presence.daily_reward` (50) once a day (UTC) after
`presence.reward_after_minutes` (10) of play that day, logged as `daily_reward` (0 turns it off). Only seconds with a
move, a turn or a command in the minute before count, so an alt that logs in and stands still gets nothing (on
2026-11-01, 1,242 players got the reward; 323 of them had played 10 minutes in all). The count isn't kept over a
restart. A player who hasn't moved, turned or run a command for `presence.afk_minutes` (5),
or typed `/afk`, is AFK: they don't count for sleeping (vanilla's `players_sleeping_percentage`) until they move or
get in a bed; `presence.afk_announce` tells everyone instead of only them. A player runs at most
`presence.spam_commands` (8) of the SMP's commands in `presence.spam_seconds` (5); operators of level 3 aren't
limited. All of it is looked at once a second.
**The auction house.** Players sell to players: a listing is a fixed price or a timed auction. The listed items
leave the seller's inventory at once; what doesn't sell, what an auction's winner gets, and what didn't fit in a
buyer's inventory wait in the player's mail (`/ah claim`, or the menu's mail page; as much as fits, the rest stays).
A purchase takes the whole listing: the listing leaves first, the items move, then the money (the seller gets the
whole price); of two players who buy at once (the game runs every click on one
thread), the second finds it gone. Bids wait in the `auction` pool: a higher bid gives the last one back at once,
raising one's own only holds the difference, and a bid in the last `snipe_seconds` (60) moves the end back to
60 seconds away. When an auction ends, the seller is paid the winning bid and the items go to the winner's mail. Sellers
and outbid players who are offline find a note when they come back. Everything (listings, bids, mail, notes) is
written in `economy.json` with the balances, in one step: a crash can't keep the money and lose the items, or the
reverse. A listing carries the whole stack (since 0.31): its enchantments, name, damage, lore, a shulker box's
or a bundle's contents and every other component, written as the game writes it in player files, and given back
exactly so (Horizon's `native_give_stack`) to the buyer, the winner, or the seller when it comes back. `/ah sell
<price> <count>` lists part of the stack in hand; the rest stays in the hand, the same. The menu shows the stack
itself (its tooltip), with the listing's price, seller and time under it. A stack is taken only if it would come
back the same (its data written and read back equal); not market charts (free at `/market`), nor a stack holding
more than 32 KiB of data (a box of books). Listings and mail from before 0.31 hold plain items and are given back as
before. Survival and adventure players only. `config.json`'s `auction`:
`enabled`, `max_listings` (8 a player), `listing_hours` (48), `auction_hours` (12),
`auction_hours_max` (24), `bid_step_percent` (5: the smallest raise), `snipe_seconds` (60). Ended listings are
looked at once a second (a pass over the open listings).
**Money in and out.** Money enters the game through what the market buys from players, the daily reward, the
casino's float (once) and operators' `/eco give` (with a reason everyone reads, logged as `eco_give`). It leaves
through what the market sells and operators' `/eco take`. There are no fees, taxes, bank, contracts or futures: no
VAT, land or wealth tax, no tax on market trades, no teleport, auction or coinflip fee. A world from before 0.30 is
cleaned up at its first start: bank savings and the margins of open futures positions go back to their owners
(`bank_closed`, `futures_closed`), and the Treasury, its insurance fund, contracts and land tax owed are dropped.
`config.json`'s old `taxes`, `bank`, `jobs` and `futures` sections and the `fee_percent` and `listing_fee_percent`
keys are ignored.
**The report.** `/eco stats` (anyone) shows the money supply (wallets and money held, each pool, the casino's pot),
what the server made today (market sales, the daily reward, `/eco give`) and destroyed (market purchases, operators'
`/eco take`), and how evenly money is spread (Gini over players
with money, the richest ten's share). At 00:00 UTC everyone reads the day before in one line (`report.daily_report`).
A snapshot of each day is kept 60 days (`report.history_days`) in `economy.json` (`ext.report`). Once a minute
`world/horizon-smp/web/economy.json` (`report.web_json`) is written off the tick thread for the site's /hub:
`{"schema": 1, "generated": <ms>, "currency", "today", "pools", "minted_today", "burned_today", "days": [...],
"daily_report": "<yesterday's line>"}`. The server's Discord relay posts only chat, joins, leaves and deaths: the
daily line reaches Discord through whoever reads that file (Kira).
Other parts reach the pools through `Economy::to_pool`, `from_pool`, `pool_move` and `settle_pool` (a bet against a
pool: the stake goes in, the payout comes out, never more than the pool holds), each one audit line.
The currency is earned in game only: it can't be bought with real money or cashed out (Mojang's usage guidelines).
## Data
@@ -61,9 +304,19 @@ folder is untouched until the mod first runs on it; it changes no block and no p
| File | Holds |
|---|---|
| `config.json` | `currency` (default `coins`), `starting_balance` (default 0), and `market`: `enabled` and `items`, each item's `buy` (what the market asks) and `sell` (what it pays), 0 closing that side; written with the defaults the first time, and again when a newer version adds keys (your values kept) |
| `economy.json` | balances by player UUID, with the name last seen, and `held`: coinflip stakes and lottery tickets being decided |
| `audit.log` | one JSON line per change, written before the change: `time` (ms), `kind` (`open`, `pay`, `eco_give`...), `amount`, `from`/`to` with their balance before, `by` for an operator; a trade's line (`trade`: both players, their items and money) comes before its swap, `trade_failed` after one that didn't go through |
| `config.json` | `currency` (default `coins`), `starting_balance` (default 0), `market`: `enabled`, `items` (each item's `buy`, what the market asks, and `sell`, what it pays, 0 closing that side), `daily_sell_coins` (2000) and `daily_sell_items` (640), 0 for no limit; `shop`: `enabled`, `title`, `categories` (`name`, `icon`, `items`); `web`: `url` (Kira's `/smp/ingest`) and `token` (empty: nothing is sent), where the pages of `web/` go; written with the defaults the first time, and again when a newer version adds keys (your values kept) |
| `economy.json` | balances by player UUID, with the name last seen; `held`: coinflip stakes and lottery tickets being decided; `pools`: the casino's pot and the auction house's bids; `ext`: other parts' state written in the same step as the money (`auction`: the listings, bids, mail and notes) |
| `places.json` | the spawn, the warps, each player's homes, and the seconds each player has played (written once a minute) |
| `nicks.json` | each player's nickname and when they last changed it |
| `staff.log` | one line per `/invsee` or `/endersee` menu in which something moved: who, whose, what was taken and put |
| `market.json` | what each player sold today (coins and items), for the daily limits |
| `market_prices.json` | each item's price factor and history (minutes for 24 h, hours for 30 days: `[time, mid, traded, sold]`), what each player bought in the last day (no selling it back), the day's summary |
| `web/market.json` | for the website (`/hub/market`, through Kira), written by a thread each minute prices move: `schema` 1, `generated`, `currency`, `tax_percent` (always 0, for older readers), `daily_summary` (`day`, `lines`), `items` (`id`, `name`, `category`, `price` (the one price; `buy` and `sell` repeat it on the sides the market trades, for older readers), `mid`, `base`, `change_1h`, `change_24h`, `volume_24h`, `day`: 5-minute points, `month`: hours, each `{step, t, price, volume}`). When `web.token` is set, the same thread POSTs each page of `web/` that changed (gzipped, `Authorization: Bearer <token>`) to `<web.url>/<name>`: Kira keeps the last one and serves it to the site; the server needs no inbound access and Kira no key to it |
| `presence.json` | the players who have joined (no welcome again), and the day each last got the daily reward |
| `board.json` | the players who hid the sidebar (`/sidebar off`) |
| `casino.json` | the day's ledger: each player's bets, wins and rounds today (the daily loss cap), the day's biggest wins, each game's totals, the pot's last sweep |
| `claims.json` | each claimed chunk (`<dimension> <chunk x> <chunk z>`) with its owner, and whom each owner trusts |
| `audit.log` | one JSON line per change, written before the change: `time` (ms), `kind` (`open`, `pay`, `grant`, `ah_listing_fee`...), `amount`, `from`/`to` with their balance before, `by` for an operator, `to_pool`/`from_pool` with `pool_before` and `reason` for a pool's movement; a trade's line (`trade`: both players, their items and money) comes before its swap, `trade_failed` after one that didn't go through |
`economy.json` is written whole to a temporary file, flushed and renamed over the old one, at most once a server tick
and when the world closes: a crash leaves the old file or the new one. If it can't be read, the economy stays off (the
@@ -72,16 +325,23 @@ commands say so) rather than start from nothing.
## Made on the hook points
It hooks only Horizon's native hook points: `native_register_commands` and `native_perform_command` (API 8), the
acting player's items (API 9), the server menus and `native_exchange_items` (API 10), the server tick and the world
lifecycle. `compatibility.json` names `"hooks": 10`.
acting player's items (API 9), the server menus and `native_exchange_items` (API 10), the players' health and
`native_run_command` (API 21), `native_destroy_block` (API 14), `native_right_click_block`, the acting player and the
action bar (API 21), `native_block_spread`, `native_piston_move`, `native_explosion_detonate`, `native_fill_bucket` and
`native_attack_entity` (API 22), `native_sidebar`, `native_tab_list` and `native_tick_stats` (API 49),
`native_set_nickname` and `native_text_allowed` (API 62), the server tick and the world lifecycle.
`compatibility.json` names `"hooks": 58`.
| File | What it is |
|---|---|
| `mixins/smp/horizon-mixin.toml` | The bundle: seven modules and the HEAD hooks |
| `mixins/smp/smp_store.rs`, `smp_economy.rs`, `smp_market.rs`, `smp_trade.rs`, `smp_games.rs`, `smp_lottery.rs` | The rules, no game types (a trade's stacks are a type parameter): added to the game as `game::smp_store`, `game::smp_economy`, `game::smp_market`, `game::smp_trade`, `game::smp_games`, `game::smp_lottery`; also this crate's modules (`src/lib.rs`), for their tests |
| `mixins/smp/horizon-mixin.toml` | The bundle: twenty-seven modules and the HEAD hooks |
| `mixins/smp/smp_store.rs`, `smp_economy.rs`, `smp_market.rs`, `smp_recipes.rs`, `smp_shop.rs`, `smp_nick.rs`, `smp_trade.rs`, `smp_games.rs`, `smp_lottery.rs`, `smp_places.rs`, `smp_claims.rs`, `smp_board.rs`, `smp_presence.rs`, `smp_auction.rs`, `smp_report.rs`, `smp_market_prices.rs`, `smp_market_menu.rs`, `smp_market_chart.rs`, `smp_invsee.rs` | The rules, no game types (a trade's stacks are a type parameter): added to the game as `game::smp_store`, `game::smp_economy`, `game::smp_market`, `game::smp_recipes`, `game::smp_shop`, `game::smp_nick`, `game::smp_trade`, `game::smp_games`, `game::smp_lottery`, `game::smp_places`, `game::smp_claims`, `game::smp_board`, `game::smp_presence`, `game::smp_auction`, `game::smp_report`, `game::smp_market_prices`, `game::smp_market_menu`, `game::smp_market_chart`, `game::smp_invsee`; also this crate's modules (`src/lib.rs`), for their tests |
| `mixins/smp/glue.rs` | The state for the open world and the edges to the game's types (`game::smp_glue`) |
| `mixins/smp/{register,command,tick,lifecycle,menu_clicked,menu_closed}.rs` | One small helper per hook |
| `mixins/smp/{register,command,tick,lifecycle,menu_clicked,menu_closed,destroy_block,right_click_block,block_spread,piston_move,explosion_detonate,fill_bucket,attack_entity}.rs` | One small helper per hook |
| `mixins/smp/smp_casino.rs`, `smp_casino_roulette.rs`, `smp_casino_slots.rs`, `smp_casino_crash.rs`, `smp_casino_blackjack.rs` | The casino's rules, menus and effects (`game::smp_casino*`) |
| `mixins/smp/casino_glue.rs` | The casino's effects for a stock client: menu items, sounds, titles, fireworks, boss bars (`game::smp_casino_glue`) |
| `proof.py` | A dedicated server built with the mod and two real clients, on fresh temporary data |
| `proof_casino.py` | The casino's proof (`SMP_PROOF=casino`): both clients play every game through the menus |
## Build and prove
+3 -3
View File
@@ -1,8 +1,8 @@
{
"game": "26.3",
"hooks": 10,
"hooks": 62,
"horizon_repository": "https://github.com/horizon-rust/horizon",
"horizon_revision": "f533c3af7d41d7ecc3b015a9fbb0dbf5eb64ae26",
"horizon_revision": "975c07d9151847e6af13afc4b5ab37b6bbe78ef5",
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
"horizon_mixins_revision": "0d6b103c70633d8fd9443d8d0acd3aadaacedb06"
"horizon_mixins_revision": "d9c83d7096b7ea5b83678da2efdfd6161f74997e"
}
+10
View File
@@ -0,0 +1,10 @@
// A player hits an entity: a claim's frames, armor stands and animals are
// kept from players its owner doesn't trust.
fn smp_attack_entity(
game: &mut crate::game::Game<'_>,
kind: &str,
pos: [f64; 3],
canceled: &mut bool,
) {
crate::game::smp_glue::attack(game, kind, pos, canceled);
}
+14
View File
@@ -0,0 +1,14 @@
// A fluid flows or fire spreads into a block: kept out of the land others
// claimed.
fn smp_block_spread(
dimension: crate::game::LevelDimension,
from: horizon_core::position::BlockPos,
to: horizon_core::position::BlockPos,
canceled: &mut bool,
) {
let dimension = crate::game::smp_glue::dimension_id(dimension);
let (from, to) = ([from.x, from.y, from.z], [to.x, to.y, to.z]);
if !crate::game::smp_glue::world_allows(dimension, from, &[to]) {
*canceled = true;
}
}
+333
View File
@@ -0,0 +1,333 @@
// The casino in the server's game: its menus, and what its effects become
// for a stock client (smp_casino's `Fx`): menu items, sounds and titles
// through the console's `playsound` and `title`, the action bar, fireworks
// and particles over a winner, a line for everyone. glue.rs calls it from the
// command, menu and tick hooks with the SMP's state already locked; the
// console commands it builds are run by glue.rs once that lock is let go.
// Added to the game as `game::smp_casino_glue`.
use crate::game::{
Game,
native_server::NativeServerPlayer,
smp_casino::{self, Casino, Color, Fx, Icon},
smp_economy::{Economy, Out, Player},
};
use horizon_client::{
items::ItemStack,
lan::data::component_nbt,
text::{self, Text},
};
/// The crash's boss bars: one a player (a bar's players can't be a list of
/// names, and tags don't reach a server's players), `smp:casino_crash_<i>`
/// for the name in `bars[i]`, and the colour they all have.
static BARS: std::sync::Mutex<(Vec<Option<String>>, &str)> =
std::sync::Mutex::new((Vec::new(), "yellow"));
/// The most bars at once; each taken off at the first hide of a run,
/// whatever it held (a run before may have left one).
const MAX_BARS: usize = 32;
static CLEARED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn bar_id(i: usize) -> String {
format!("smp:casino_crash_{i}")
}
fn color(c: Color) -> u32 {
match c {
Color::White => text::WHITE,
Color::Gray => text::GRAY,
Color::DarkGray => text::DARK_GRAY,
Color::Gold => text::GOLD,
Color::Yellow => text::YELLOW,
Color::Green => text::GREEN,
Color::Red => text::RED,
Color::Aqua => text::AQUA,
}
}
fn stack(icon: Icon) -> ItemStack {
let mut stack = ItemStack {
item: icon.item.into(),
count: u32::from(icon.count.clamp(1, 99)),
damage: 0,
extra: None,
};
let title =
serde_json::json!({ "text": icon.name, "italic": false, "color": icon.color.name() });
stack.data_mut().custom_name = Some(component_nbt(&title));
stack
}
fn items(casino: &Casino, economy: &Economy, uuid: u128, now: i64) -> Vec<Option<ItemStack>> {
casino
.layout(economy, uuid, now)
.into_iter()
.map(|c| c.map(stack))
.collect()
}
/// Opens (again) the player's casino menu on its view: a new window, titled
/// for the game. False when it couldn't open.
fn open(game: &mut Game<'_>, casino: &mut Casino, economy: &Economy, uuid: u128, now: i64) -> bool {
let Some(view) = casino.seats.get(&uuid).map(|s| s.view) else {
return false;
};
let items = items(casino, economy, uuid, now);
let title = Text::literal(view.title()).with_color(text::DARK_GRAY);
match game.native_open_menu(uuid, smp_casino::ROWS, title, items) {
Some(w) => {
if let Some(s) = casino.seats.get_mut(&uuid) {
s.window = Some(w);
}
true
}
None => {
casino.seats.remove(&uuid);
false
}
}
}
/// `/casino ...` (inside the player's command).
pub(in crate::game) fn command(
game: &mut Game<'_>,
casino: &mut Casino,
economy: &mut Economy,
me: &Player,
now: i64,
line: &str,
) -> Vec<Out> {
let Some((mut out, view)) = casino.command(economy, me, now, line) else {
return Vec::new();
};
if let Some(view) = view {
casino.sit(me.uuid, &me.name, view);
if !open(game, casino, economy, me.uuid, now) {
out.push(Out::Error("The casino couldn't open.".into()));
}
}
out
}
/// A click in a player's casino menu: the console commands its effects need,
/// for after the SMP's lock.
pub(in crate::game) fn clicked(
game: &mut Game<'_>,
casino: &mut Casino,
economy: &mut Economy,
uuid: u128,
slot: i32,
shift: bool,
) -> Vec<String> {
let Ok(slot) = usize::try_from(slot) else {
return Vec::new();
};
if slot >= smp_casino::SIZE {
return Vec::new();
}
let now = chrono::Utc::now().timestamp_millis();
economy.now = now;
let fx = casino.click(economy, uuid, slot, shift, now);
apply(game, casino, economy, fx, now)
}
/// The casino's share of the server's tick: frames, rounds that end, the
/// pot's float (once). The console commands, for after the lock.
pub(in crate::game) fn tick(
game: &mut Game<'_>,
casino: &mut Casino,
economy: &mut Economy,
now: i64,
online: Option<&[u128]>,
) -> Vec<String> {
let mut fx = casino.tick(economy, now);
// Once a second: crash bets of players who left.
if let Some(online) = online {
fx.extend(casino.gone(economy, online, now));
}
if fx.is_empty() {
return Vec::new();
}
apply(game, casino, economy, fx, now)
}
/// Carries out `fx`: menus and lines now, console commands returned.
fn apply(
game: &mut Game<'_>,
casino: &mut Casino,
economy: &Economy,
fx: Vec<Fx>,
now: i64,
) -> Vec<String> {
let mut commands = Vec::new();
let mut players: Option<Vec<NativeServerPlayer>> = None;
let mut shown = Vec::new();
for f in fx {
// Where a player is, asked once for the whole batch.
let mut find = |uuid: u128, game: &Game<'_>| -> Option<NativeServerPlayer> {
players
.get_or_insert_with(|| game.native_server_players())
.iter()
.find(|p| p.uuid == uuid)
.cloned()
};
match f {
Fx::Out(Out::Tell(uuid, s)) => {
game.native_tell(uuid, Text::literal(s).with_color(text::GOLD));
}
Fx::Out(Out::All(s)) => game.native_broadcast(Text::literal(s).with_color(text::GOLD)),
Fx::Out(Out::Reply(s) | Out::Error(s)) => log::info!("[smp] casino: {s}"),
Fx::Sound {
to,
sound,
volume,
pitch,
} => {
if let Some(p) = find(to, game) {
let [x, y, z] = p.position;
commands.push(format!(
"execute in {} run playsound {sound} master {} {x:.2} {y:.2} {z:.2} {volume} {pitch}",
p.dimension, p.name
));
}
}
Fx::Title {
to,
title,
subtitle,
color: c,
} => {
if let Some(p) = find(to, game) {
let line = |s: &str, bold: bool| {
serde_json::json!({ "text": s, "color": c.name(), "bold": bold })
.to_string()
};
commands.push(format!("title {} times 5 40 15", p.name));
commands.push(format!(
"title {} subtitle {}",
p.name,
line(&subtitle, false)
));
commands.push(format!("title {} title {}", p.name, line(&title, true)));
}
}
Fx::Bar {
to,
text: s,
color: c,
} => {
game.native_action_bar(to, Text::literal(s).with_color(color(c)));
}
Fx::Fireworks(to) => {
if let Some(p) = find(to, game) {
let [x, y, z] = p.position;
// High enough over the player that their blast (5 blocks) never hurts.
for (dx, dz, colours) in [
(-3.0, 0.0, "16766720,16733525"),
(3.0, 1.5, "5635925,16777045"),
(0.0, -3.0, "5636095,16733695"),
] {
commands.push(format!(
"execute in {} run summon minecraft:firework_rocket {:.2} {:.2} {:.2} {{LifeTime:18,FireworksItem:{{id:\"minecraft:firework_rocket\",count:1,components:{{\"minecraft:fireworks\":{{flight_duration:1b,explosions:[{{shape:\"large_ball\",colors:[I;{colours}],has_trail:true,has_twinkle:true}}]}}}}}}}}",
p.dimension,
x + dx,
y + 9.0,
z + dz
));
}
commands.push(format!(
"execute in {} run particle minecraft:totem_of_undying {x:.2} {:.2} {z:.2} 0.6 1 0.6 0.4 60 normal",
p.dimension,
y + 1.0
));
}
}
Fx::Announce(s) => {
log::info!("[smp] casino: {s}");
game.native_broadcast(
Text::literal(format!("[Casino] {s}")).with_color(text::GOLD),
);
}
Fx::Show(uuid) => {
// One picture per menu per batch.
if !shown.contains(&uuid)
&& casino.seats.get(&uuid).is_some_and(|s| s.window.is_some())
{
shown.push(uuid);
let items = items(casino, economy, uuid, now);
game.native_set_menu_items(uuid, items);
}
}
Fx::Open(uuid) => {
shown.push(uuid);
open(game, casino, economy, uuid, now);
}
Fx::Boss {
text: line,
color: c,
value,
} => {
let mut bars = BARS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(c) = c {
bars.1 = match c {
Color::Green => "green",
Color::Red => "red",
_ => "yellow",
};
}
let name = serde_json::json!({ "text": line, "color": "white" });
for (i, _) in bars.0.iter().enumerate().filter(|(_, b)| b.is_some()) {
let id = bar_id(i);
commands.push(format!("bossbar set {id} name {name}"));
commands.push(format!("bossbar set {id} value {value}"));
if c.is_some() {
commands.push(format!("bossbar set {id} color {}", bars.1));
}
}
}
Fx::BossPlayers(names) => {
let mut bars = BARS.lock().unwrap_or_else(|e| e.into_inner());
let colour = bars.1;
for (i, bar) in bars.0.iter_mut().enumerate() {
if bar.as_ref().is_some_and(|n| !names.contains(n)) {
*bar = None;
commands.push(format!("bossbar remove {}", bar_id(i)));
}
}
for name in names {
if bars.0.iter().any(|b| b.as_ref() == Some(&name)) {
continue;
}
let i = match bars.0.iter().position(Option::is_none) {
Some(i) => i,
None if bars.0.len() < MAX_BARS => {
bars.0.push(None);
bars.0.len() - 1
}
None => continue,
};
let id = bar_id(i);
commands.push(format!("bossbar add {id} \"\""));
commands.push(format!("bossbar set {id} max 100"));
commands.push(format!("bossbar set {id} color {colour}"));
commands.push(format!("bossbar set {id} players {name}"));
bars.0[i] = Some(name);
}
}
Fx::BossHide => {
// Every bar taken off.
let mut bars = BARS.lock().unwrap_or_else(|e| e.into_inner());
let all = !CLEARED.swap(true, std::sync::atomic::Ordering::Relaxed);
let n = if all { MAX_BARS } else { bars.0.len() };
for i in 0..n {
if all || bars.0[i].is_some() {
commands.push(format!("bossbar remove {}", bar_id(i)));
}
}
bars.0.clear();
bars.1 = "yellow";
}
}
}
commands
}
+3
View File
@@ -5,6 +5,9 @@ fn smp_perform_command(
command: &str,
ran: &mut bool,
) {
if !*ran {
crate::game::smp_invsee_glue::perform(game, player, command, ran);
}
if !*ran {
crate::game::smp_glue::perform(game, player, command, ran);
}
+4
View File
@@ -0,0 +1,4 @@
// A player breaks a block: kept out of the land others claimed.
fn smp_destroy_block(game: &mut crate::game::Game<'_>, pos: [i32; 3], canceled: &mut bool) {
crate::game::smp_glue::guard(game, pos, canceled);
}
@@ -0,0 +1,8 @@
// An explosion's blocks: claimed land is spared.
fn smp_explosion_detonate(
game: &crate::game::Game<'_>,
dimension: &str,
blocks: &mut Vec<[i32; 3]>,
) {
crate::game::smp_glue::explosion(game, dimension, blocks);
}
+4
View File
@@ -0,0 +1,4 @@
// A player fills or empties a bucket: kept out of the land others claimed.
fn smp_fill_bucket(game: &mut crate::game::Game<'_>, pos: [i32; 3], canceled: &mut bool) {
crate::game::smp_glue::guard(game, pos, canceled);
}
File diff suppressed because it is too large. Load diff
+206 -4
View File
@@ -1,12 +1,12 @@
[mod]
id = "smp"
version = "0.5.0"
version = "0.31.0"
priority = 100
# The commit of horizon main it was tested on. It uses only the native hook points (hooks 10 in
# compatibility.json): it builds on any Horizon that has them.
# The commit of horizon it was tested on: it needs horizon#6106 (merged at 975c07d): native_inventory,
# native_move_stack, native_send_map, lore on the wire.
[compatibility]
horizon_revision = "f533c3af7d41d7ecc3b015a9fbb0dbf5eb64ae26"
horizon_revision = "975c07d9151847e6af13afc4b5ab37b6bbe78ef5"
[[module]]
id = "store"
@@ -26,6 +26,48 @@ file = "crates/horizon-client/src/game.rs"
name = "smp_market"
source = "smp_market.rs"
[[module]]
id = "market_prices"
file = "crates/horizon-client/src/game.rs"
name = "smp_market_prices"
source = "smp_market_prices.rs"
[[module]]
id = "market_chart"
file = "crates/horizon-client/src/game.rs"
name = "smp_market_chart"
source = "smp_market_chart.rs"
[[module]]
id = "market_menu"
file = "crates/horizon-client/src/game.rs"
name = "smp_market_menu"
source = "smp_market_menu.rs"
[[module]]
id = "recipes"
file = "crates/horizon-client/src/game.rs"
name = "smp_recipes"
source = "smp_recipes.rs"
[[module]]
id = "shop"
file = "crates/horizon-client/src/game.rs"
name = "smp_shop"
source = "smp_shop.rs"
[[module]]
id = "report"
file = "crates/horizon-client/src/game.rs"
name = "smp_report"
source = "smp_report.rs"
[[module]]
id = "auction"
file = "crates/horizon-client/src/game.rs"
name = "smp_auction"
source = "smp_auction.rs"
[[module]]
id = "trade"
file = "crates/horizon-client/src/game.rs"
@@ -44,6 +86,84 @@ file = "crates/horizon-client/src/game.rs"
name = "smp_lottery"
source = "smp_lottery.rs"
[[module]]
id = "casino"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino"
source = "smp_casino.rs"
[[module]]
id = "casino_roulette"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino_roulette"
source = "smp_casino_roulette.rs"
[[module]]
id = "casino_slots"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino_slots"
source = "smp_casino_slots.rs"
[[module]]
id = "casino_crash"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino_crash"
source = "smp_casino_crash.rs"
[[module]]
id = "casino_blackjack"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino_blackjack"
source = "smp_casino_blackjack.rs"
[[module]]
id = "casino_glue"
file = "crates/horizon-client/src/game.rs"
name = "smp_casino_glue"
source = "casino_glue.rs"
[[module]]
id = "nick"
file = "crates/horizon-client/src/game.rs"
name = "smp_nick"
source = "smp_nick.rs"
[[module]]
id = "places"
file = "crates/horizon-client/src/game.rs"
name = "smp_places"
source = "smp_places.rs"
[[module]]
id = "board"
file = "crates/horizon-client/src/game.rs"
name = "smp_board"
source = "smp_board.rs"
[[module]]
id = "presence"
file = "crates/horizon-client/src/game.rs"
name = "smp_presence"
source = "smp_presence.rs"
[[module]]
id = "claims"
file = "crates/horizon-client/src/game.rs"
name = "smp_claims"
source = "smp_claims.rs"
[[module]]
id = "invsee"
file = "crates/horizon-client/src/game.rs"
name = "smp_invsee"
source = "smp_invsee.rs"
[[module]]
id = "invsee_glue"
file = "crates/horizon-client/src/game.rs"
name = "smp_invsee_glue"
source = "invsee_glue.rs"
[[module]]
id = "glue"
file = "crates/horizon-client/src/game.rs"
@@ -113,3 +233,85 @@ bindings = [
{ name = "uuid", ty = "u128", value = "uuid" },
{ name = "window", ty = "i32", value = "window" },
]
[[injection]]
id = "destroy_block"
kind = "HEAD"
target = "horizon_client::game::native_hooks::Game<'_>::native_destroy_block"
helper = "destroy_block.rs"
bindings = [
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
{ name = "pos", ty = "[i32; 3]", value = "pos" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[injection]]
id = "right_click_block"
kind = "HEAD"
target = "horizon_client::game::native_hooks::Game<'_>::native_right_click_block"
helper = "right_click_block.rs"
bindings = [
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
{ name = "pos", ty = "[i32; 3]", value = "pos" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[injection]]
id = "block_spread"
kind = "HEAD"
target = "horizon_client::game::native_hooks::GameLevel<'_>::native_block_spread"
helper = "block_spread.rs"
bindings = [
{ name = "dimension", ty = "crate::game::LevelDimension", value = "dimension" },
{ name = "from", ty = "horizon_core::position::BlockPos", value = "from" },
{ name = "to", ty = "horizon_core::position::BlockPos", value = "to" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[injection]]
id = "piston_move"
kind = "HEAD"
target = "horizon_client::game::native_hooks::GameLevel<'_>::native_piston_move"
helper = "piston_move.rs"
bindings = [
{ name = "dimension", ty = "crate::game::LevelDimension", value = "dimension" },
{ name = "piston", ty = "horizon_core::position::BlockPos", value = "piston" },
{ name = "direction", ty = "[i32; 3]", value = "direction" },
{ name = "push", ty = "&[horizon_core::position::BlockPos]", value = "push" },
{ name = "destroy", ty = "&[horizon_core::position::BlockPos]", value = "destroy" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[injection]]
id = "explosion_detonate"
kind = "HEAD"
target = "horizon_client::game::native_hooks::Game<'_>::native_explosion_detonate"
helper = "explosion_detonate.rs"
bindings = [
{ name = "game", ty = "&crate::game::Game<'_>", value = "&*self" },
{ name = "dimension", ty = "&str", value = "dimension" },
{ name = "blocks", ty = "&mut Vec<[i32; 3]>", value = "&mut *blocks" },
]
[[injection]]
id = "fill_bucket"
kind = "HEAD"
target = "horizon_client::game::native_hooks::Game<'_>::native_fill_bucket"
helper = "fill_bucket.rs"
bindings = [
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
{ name = "pos", ty = "[i32; 3]", value = "pos" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
[[injection]]
id = "attack_entity"
kind = "HEAD"
target = "horizon_client::game::native_hooks::Game<'_>::native_attack_entity"
helper = "attack_entity.rs"
bindings = [
{ name = "game", ty = "&mut crate::game::Game<'_>", value = "&mut *self" },
{ name = "kind", ty = "&str", value = "kind" },
{ name = "pos", ty = "[f64; 3]", value = "pos" },
{ name = "canceled", ty = "&mut bool", value = "&mut *canceled" },
]
+314
View File
@@ -0,0 +1,314 @@
// `/invsee` and `/endersee` in the server's game (smp_invsee's rules): the
// menus open, their clicks and closings, the inventories through the native
// helpers (`native_inventory`, `native_move_stack`), each move in the SMP's
// audit log, the staff's summary in the server log, `staff.log` and the
// chat of the other operators on. Its own state, apart from the SMP's: the
// tick costs one atomic load while no menu is open. Added to the game as
// `game::smp_invsee_glue`.
use crate::game::{
Game,
native_inventories::NativeInventoryKind,
native_menus::native_menu_inventory_slot,
native_server::{NativeCommand, NativeServerPlayer},
smp_economy::{Out, Player},
smp_glue,
smp_invsee::{self, Cell, Invsee, Kind, Move},
smp_store,
};
use horizon_client::{
items::ItemStack,
lan::data::component_nbt,
text::{self, Text},
};
use std::sync::{
Mutex,
atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
};
struct State {
invsee: Invsee<ItemStack>,
/// Asked in a command, opened at the next tick: (operator, target UUID,
/// real name, which).
pending: Vec<(u128, u128, String, Kind)>,
}
static STATE: Mutex<Option<State>> = Mutex::new(None);
/// A menu open or one to open: the tick has work.
static ACTIVE: AtomicBool = AtomicBool::new(false);
static TICKS: AtomicU64 = AtomicU64::new(0);
fn state() -> std::sync::MutexGuard<'static, Option<State>> {
let mut s = STATE.lock().unwrap_or_else(|e| e.into_inner());
s.get_or_insert_with(|| State {
invsee: Invsee::default(),
pending: Vec::new(),
});
s
}
impl smp_invsee::Stack for ItemStack {
fn id(&self) -> String {
self.item.to_string()
}
fn count(&self) -> u32 {
self.count
}
}
fn native(kind: Kind) -> NativeInventoryKind {
match kind {
Kind::Player => NativeInventoryKind::Player,
Kind::Ender => NativeInventoryKind::Ender,
}
}
/// The game's inventories, for the rules.
struct GameWorld<'a, 'g>(&'a mut Game<'g>);
impl smp_invsee::World<ItemStack> for GameWorld<'_, '_> {
fn read(&self, uuid: u128, kind: Kind) -> Result<(Vec<Option<ItemStack>>, bool), String> {
self.0.native_inventory(uuid, native(kind)).map(|i| (i.slots, i.online))
}
fn move_stack(&mut self, from: (u128, Kind, usize), expected: &ItemStack, to: (u128, Kind)) -> Result<(), String> {
self.0.native_move_stack((from.0, native(from.1), from.2), expected, (to.0, native(to.1)))
}
}
pub fn commands(commands: &mut Vec<NativeCommand>) {
for usage in ["invsee <player:word>", "endersee <player:word>"] {
commands.push(NativeCommand::op(usage, smp_invsee::LEVEL));
}
}
/// `/invsee` and `/endersee`; anything else is left to the SMP.
pub(in crate::game) fn perform(game: &mut Game<'_>, player: &NativeServerPlayer, line: &str, ran: &mut bool) {
let word = line.split(' ').next().unwrap_or_default();
if !smp_invsee::COMMANDS.contains(&word) || !game.native_on(smp_glue::ID) {
return;
}
*ran = true;
let me = Player {
uuid: player.uuid,
name: player.name.clone(),
level: player.permission,
};
let find = |name: &str| smp_glue::find_player(game, name);
let mut guard = state();
let s = guard.as_mut().expect("made");
let (out, open) = s.invsee.command(&me, &find, line).unwrap_or_default();
if let Some((target, name, kind)) = open {
s.pending.retain(|p| p.0 != me.uuid);
s.pending.push((me.uuid, target, name, kind));
ACTIVE.store(true, Relaxed);
}
drop(guard);
for o in out {
match o {
Out::Error(e) => game.native_feedback(Text::literal(e).with_color(text::RED)),
Out::Reply(r) => game.native_feedback(Text::literal(r)),
_ => {}
}
}
}
fn named(item: &str, name: &str, color: &str) -> ItemStack {
let mut stack = ItemStack {
item: item.into(),
count: 1,
damage: 0,
extra: None,
};
let title = serde_json::json!({ "text": name, "italic": false, "color": color });
stack.data_mut().custom_name = Some(component_nbt(&title));
stack
}
fn items(invsee: &Invsee<ItemStack>, op: u128) -> Vec<Option<ItemStack>> {
invsee
.layout(op)
.into_iter()
.map(|c| match c {
Cell::Empty => None,
Cell::Stack(s) => Some(s),
Cell::Filler => Some(named("minecraft:gray_stained_glass_pane", " ", "gray")),
Cell::Label(l) => Some(named("minecraft:light_gray_stained_glass_pane", l, "gray")),
Cell::Close => Some(named("minecraft:barrier", "Close", "red")),
Cell::Info { name, online, busy } => {
let line = match &busy {
Some(b) => format!("{name}: {b} (nothing moves now)"),
None if online => format!("{name} (online): click theirs to take, yours to give"),
None => format!("{name} (offline, their saved file): click theirs to take, yours to give"),
};
Some(named("minecraft:player_head", &line, if busy.is_some() { "red" } else { "yellow" }))
}
})
.collect()
}
fn title(name: &str, kind: Kind) -> Text {
Text::literal(match kind {
Kind::Player => format!("{name}'s inventory"),
Kind::Ender => format!("{name}'s ender chest"),
})
}
/// The audit line of a move: written before it is made ("asked"), and
/// again when the game refused it.
fn audit(dir: &std::path::Path, m: &Move, outcome: &str) -> Result<(), String> {
smp_glue::audit(
dir,
serde_json::json!({
"kind": if m.take { "invsee_take" } else { "invsee_put" },
"op": smp_store::uuid_string(m.op),
"target": smp_store::uuid_string(m.target),
"inventory": match m.kind { Kind::Player => "inventory", Kind::Ender => "ender_chest" },
"item": m.item,
"count": m.count,
"outcome": outcome,
}),
)
}
/// The staff's line: the server log, `staff.log`, the other operators on.
fn staff(game: &mut Game<'_>, op: u128, line: &str) {
log::warn!("[smp] staff: {line}");
if let Some(dir) = game.native_world_dir() {
use std::io::Write;
let path = dir.join(smp_store::DIR).join("staff.log");
let stamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
let _ = writeln!(f, "{stamp} {line}");
}
}
for p in game.native_server_players() {
if p.uuid != op && p.permission >= smp_invsee::LEVEL {
game.native_tell(p.uuid, Text::literal(format!("[Staff] {line}")).with_color(text::GRAY));
}
}
}
/// Each tick: one load while nothing is open. Menus to open now; once a
/// second, every open menu read again, and those of operators gone closed.
pub(in crate::game) fn tick(game: &mut Game<'_>) {
if !ACTIVE.load(Relaxed) {
return;
}
let n = TICKS.fetch_add(1, Relaxed);
let mut guard = state();
let s = guard.as_mut().expect("made");
let mut tell = Vec::new();
for (op, target, name, kind) in std::mem::take(&mut s.pending) {
let opened = s.invsee.open(op, target, &name, kind, &GameWorld(game));
match opened {
Ok(()) => {
let window = game.native_open_menu(op, smp_invsee::ROWS, title(&name, kind), items(&s.invsee, op));
match window {
Some(w) => s.invsee.views.get_mut(&op).expect("opened").window = Some(w),
None => {
s.invsee.views.remove(&op);
}
}
}
Err(e) => tell.push((op, format!("Can't open {name}'s inventory: {e}."))),
}
}
let mut summaries = Vec::new();
if n % 20 == 0 {
let on: Vec<(u128, String)> = game.native_server_players().into_iter().map(|p| (p.uuid, p.name)).collect();
let ops: Vec<u128> = s.invsee.views.keys().copied().collect();
for op in ops {
match on.iter().find(|p| p.0 == op) {
None => summaries.extend(s.invsee.close(op, &smp_store::uuid_string(op)).map(|l| (op, l))),
Some(_) => {
if s.invsee.refresh(op, &GameWorld(game)) {
game.native_set_menu_items(op, items(&s.invsee, op));
}
}
}
}
}
ACTIVE.store(!s.invsee.views.is_empty() || !s.pending.is_empty(), Relaxed);
drop(guard);
for (op, line) in tell {
game.native_tell(op, Text::literal(line).with_color(text::RED));
}
for (op, line) in summaries {
staff(game, op, &line);
}
}
/// A click in an `/invsee` menu: true when it was one.
pub(in crate::game) fn menu_clicked(game: &mut Game<'_>, uuid: u128, window: i32, slot: i32, mode: i32) -> bool {
let mut guard = state();
let s = guard.as_mut().expect("made");
if s.invsee.views.get(&uuid).is_none_or(|v| v.window != Some(window)) {
return false;
}
let Some(dir) = game.native_world_dir() else {
return true;
};
if !matches!(mode, 0 | 1) {
return true;
}
let mut asked: Option<Move> = None;
// Each move is in the audit log before it is made; one the log
// refuses isn't made.
let mut record = |m: &Move| {
audit(&dir, m, "asked")?;
asked = Some(m.clone());
Ok(())
};
let own = native_menu_inventory_slot(smp_invsee::SIZE, slot);
let mut world = GameWorld(game);
let result = match own {
Some(i) => match world.0.native_player_stacks(uuid).and_then(|st| st.get(i).cloned().flatten()) {
Some(stack) => s.invsee.click_own(uuid, i, &stack, &mut world, &mut record).map(|_| false),
None => Ok(false),
},
None => match usize::try_from(slot) {
Ok(m) if m < smp_invsee::SIZE => s.invsee.click_menu(uuid, m, &mut world, &mut record).map(|c| c == Some(true)),
_ => Ok(false),
},
};
if let (Some(m), Err(e)) = (&asked, &result)
&& let Err(err) = audit(&dir, m, &format!("refused: {e}"))
{
log::error!("[smp] invsee audit: {err}");
}
let name = game.native_server_players().into_iter().find(|p| p.uuid == uuid).map(|p| p.name);
let close = matches!(result, Ok(true));
let summary = if close { s.invsee.close(uuid, name.as_deref().unwrap_or("?")) } else { None };
let refreshed = (!close).then(|| items(&s.invsee, uuid));
ACTIVE.store(!s.invsee.views.is_empty() || !s.pending.is_empty(), Relaxed);
drop(guard);
if let Err(e) = &result {
game.native_tell(uuid, Text::literal(format!("Nothing moved: {e}.")).with_color(text::RED));
}
if close {
game.native_close_menu(uuid);
} else if let Some(items) = refreshed {
game.native_set_menu_items(uuid, items);
}
if let Some(line) = summary {
staff(game, uuid, &line);
}
true
}
/// An `/invsee` menu closed: true when it was one.
pub(in crate::game) fn menu_closed(game: &mut Game<'_>, uuid: u128, window: i32) -> bool {
let mut guard = state();
let s = guard.as_mut().expect("made");
if s.invsee.views.get(&uuid).is_none_or(|v| v.window != Some(window)) {
return false;
}
let name = game.native_server_players().into_iter().find(|p| p.uuid == uuid).map(|p| p.name);
let summary = s.invsee.close(uuid, name.as_deref().unwrap_or("?"));
ACTIVE.store(!s.invsee.views.is_empty() || !s.pending.is_empty(), Relaxed);
drop(guard);
if let Some(line) = summary {
staff(game, uuid, &line);
}
true
}
+3 -1
View File
@@ -1,4 +1,6 @@
// A click in a menu a native mod opened: the SMP's trade menu, when it is one.
fn smp_menu_clicked(game: &mut crate::game::Game<'_>, uuid: u128, window: i32, slot: i32, mode: i32) {
crate::game::smp_glue::menu_clicked(game, uuid, window, slot, mode);
if !crate::game::smp_invsee_glue::menu_clicked(game, uuid, window, slot, mode) {
crate::game::smp_glue::menu_clicked(game, uuid, window, slot, mode);
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
// A menu a native mod opened closed: a trade's ends, nothing moved.
fn smp_menu_closed(game: &mut crate::game::Game<'_>, uuid: u128, window: i32) {
crate::game::smp_glue::menu_closed(game, uuid, window);
if !crate::game::smp_invsee_glue::menu_closed(game, uuid, window) {
crate::game::smp_glue::menu_closed(game, uuid, window);
}
}
+24
View File
@@ -0,0 +1,24 @@
// A piston pushes, pulls or destroys blocks: kept out of the land others
// claimed.
fn smp_piston_move(
dimension: crate::game::LevelDimension,
piston: horizon_core::position::BlockPos,
direction: [i32; 3],
push: &[horizon_core::position::BlockPos],
destroy: &[horizon_core::position::BlockPos],
canceled: &mut bool,
) {
let dimension = crate::game::smp_glue::dimension_id(dimension);
let step = |p: &horizon_core::position::BlockPos| {
[p.x + direction[0], p.y + direction[1], p.z + direction[2]]
};
let touched: Vec<[i32; 3]> = push
.iter()
.flat_map(|p| [[p.x, p.y, p.z], step(p)])
.chain(destroy.iter().map(|p| [p.x, p.y, p.z]))
.collect();
let piston = [piston.x, piston.y, piston.z];
if !crate::game::smp_glue::world_allows(dimension, piston, &touched) {
*canceled = true;
}
}
+1
View File
@@ -1,4 +1,5 @@
// The SMP's commands in every player's command tree.
fn smp_register_commands(commands: &mut Vec<crate::game::native_server::NativeCommand>) {
crate::game::smp_glue::commands(commands);
crate::game::smp_invsee_glue::commands(commands);
}
@@ -0,0 +1,4 @@
// A player right clicks a block: kept out of the land others claimed.
fn smp_right_click_block(game: &mut crate::game::Game<'_>, pos: [i32; 3], canceled: &mut bool) {
crate::game::smp_glue::guard(game, pos, canceled);
}
File diff suppressed because it is too large. Load diff
+518
View File
@@ -0,0 +1,518 @@
// The SMP's screens: a scoreboard sidebar, the player's own block first (their
// name or nickname, balance, playtime, the players on), then the server's
// address, which a player hides with `/sidebar off` (kept in `board.json`);
// and the Tab list's header and footer (the server's name, its TPS and the
// players on). Nothing happens per tick: the balances are read once for
// everyone when one changed (at most once each `min_seconds`) or every
// `refresh_seconds`; the players' own lines are looked
// at on the same `refresh_seconds`, and a sidebar is sent only to the players
// whose lines changed and to newcomers. The Tab list goes out every
// `tab_refresh_seconds`, and to a player who just joined. No game types. Added to the game as `game::smp_board`;
// also a module of this crate, for its tests.
use super::smp_economy::{Out, Player};
use super::smp_store;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
/// `config.json`'s `board` section.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
/// The sidebar (each player may hide it).
pub sidebar: bool,
pub title: String,
/// Its last line, the server's address ("": none).
pub footer: String,
/// The balances are read again, and the players' own lines looked at,
/// at least this often.
pub refresh_seconds: i64,
/// After a balance changed, at most this often.
pub min_seconds: i64,
/// The Tab list's header and footer.
pub tab: bool,
/// `{server}`, `{tps}`, `{mspt}`, `{players}` are filled in.
pub tab_header: String,
pub tab_footer: String,
pub server_name: String,
pub tab_refresh_seconds: i64,
}
impl Default for Config {
fn default() -> Self {
Self {
sidebar: true,
title: "Horizon SMP".into(),
footer: "play.playhorizon.dev".into(),
refresh_seconds: 60,
min_seconds: 5,
tab: true,
tab_header: "{server}".into(),
tab_footer: "TPS {tps} · {players} online".into(),
server_name: "Horizon SMP".into(),
tab_refresh_seconds: 5,
}
}
}
/// SMP 0.13's sidebar title (`Richest players`), moved to today's when a
/// server never changed it. True when something changed.
pub fn migrate(config: &mut Config) -> bool {
if config.title == "Richest players" {
config.title = Config::default().title;
return true;
}
false
}
/// `board.json`.
#[derive(Default, Serialize, Deserialize)]
#[serde(default)]
struct Data {
/// The players who hid the sidebar, by UUID.
hidden: BTreeSet<String>,
}
/// A line of a player's sidebar.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Line {
/// The player's name, or their nickname.
Name(String),
/// A value of theirs or the server's: `Balance`, `Playtime`, `Online`.
Stat { label: &'static str, value: String },
/// An empty line.
Gap,
/// The server's address.
Footer(String),
}
/// What a player's own lines show, from the other modules.
pub struct Me {
/// Their nickname, else their name.
pub name: String,
pub played_seconds: i64,
}
/// `2h 05m`, `12m`: the sidebar's playtime.
pub fn short_duration(seconds: i64) -> String {
let (h, m) = (seconds / 3600, seconds % 3600 / 60);
if h > 0 {
format!("{h}h {m:02}m")
} else {
format!("{m}m")
}
}
pub struct Board {
pub config: Config,
path: PathBuf,
data: Data,
changed: bool,
/// Every account's balance.
balances: HashMap<u128, i64>,
/// When the balances were read, and at which economy revision.
built_at: Option<i64>,
built_revision: u64,
/// What each player on was last sent.
sent: HashMap<u128, Vec<Line>>,
/// Built since the last `updates`: every player's lines are looked at.
stale: bool,
/// When the players' own lines were last looked at.
personal_at: Option<i64>,
tab_at: Option<i64>,
tab_sent: HashSet<u128>,
}
/// `1,250`, `125.4k`, `3.2M`: short enough for the sidebar's right side.
pub fn compact(n: i64) -> String {
let a = n.unsigned_abs();
let sign = if n < 0 { "-" } else { "" };
if a < 100_000 {
let digits = a.to_string();
let mut out = String::new();
for (i, c) in digits.chars().enumerate() {
if i > 0 && (digits.len() - i).is_multiple_of(3) {
out.push(',');
}
out.push(c);
}
return format!("{sign}{out}");
}
let (value, unit) = match a {
100_000..1_000_000 => (a as f64 / 1e3, "k"),
1_000_000..1_000_000_000 => (a as f64 / 1e6, "M"),
_ => (a as f64 / 1e9, "B"),
};
format!("{sign}{value:.1}{unit}")
}
impl Board {
/// `board.json` in `dir`; an error when it exists but can't be read.
pub fn open(dir: &Path, config: Config) -> Result<Self, String> {
let path = dir.join("board.json");
let data = smp_store::read_json(&path)?;
Ok(Self {
config,
path,
data,
changed: false,
balances: HashMap::new(),
built_at: None,
built_revision: 0,
sent: HashMap::new(),
stale: false,
personal_at: None,
tab_at: None,
tab_sent: HashSet::new(),
})
}
pub fn save(&mut self) -> Result<(), String> {
if self.changed {
smp_store::write_json(&self.path, &self.data)?;
self.changed = false;
}
Ok(())
}
pub fn hidden(&self, uuid: u128) -> bool {
self.data.hidden.contains(&smp_store::uuid_string(uuid))
}
/// Whether the balances should be read again: the economy changed
/// (`revision`) and `min_seconds` passed, or `refresh_seconds` passed.
pub fn due(&self, now: i64, revision: u64) -> bool {
if !self.config.sidebar {
return false;
}
let Some(at) = self.built_at else {
return true;
};
let since = now - at;
(revision != self.built_revision && since >= self.config.min_seconds * 1000)
|| since >= self.config.refresh_seconds * 1000
}
/// The balances from the accounts (UUID, name, balance).
pub fn rebuild(&mut self, accounts: Vec<(u128, String, i64)>, now: i64, revision: u64) {
self.balances = accounts.into_iter().map(|(u, _, b)| (u, b)).collect();
self.built_at = Some(now);
self.built_revision = revision;
self.stale = true;
}
/// `uuid`'s sidebar: their own block, then the address.
pub fn lines(&self, uuid: u128, me: &Me, online: usize) -> Vec<Line> {
let balance = self.balances.get(&uuid).copied().unwrap_or(0);
let mut lines = vec![
Line::Name(me.name.clone()),
Line::Stat {
label: "Balance",
value: compact(balance),
},
Line::Stat {
label: "Playtime",
value: short_duration(me.played_seconds),
},
Line::Stat {
label: "Online",
value: online.to_string(),
},
];
if !self.config.footer.is_empty() {
lines.push(Line::Gap);
lines.push(Line::Footer(self.config.footer.clone()));
}
lines
}
/// The sidebars to send now, to the players `on`: those who joined, and
/// after a rebuild or each `refresh_seconds` those whose lines changed.
/// `me` is asked only for the players looked at. Players who hid it get
/// nothing; players gone are forgotten (they get it again on return).
pub fn updates(
&mut self,
now: i64,
on: &[u128],
me: impl Fn(u128) -> Me,
) -> Vec<(u128, Vec<Line>)> {
self.sent.retain(|u, _| on.contains(u));
self.tab_sent.retain(|u| on.contains(u));
if !self.config.sidebar || self.built_at.is_none() {
return Vec::new();
}
let personal = self
.personal_at
.is_none_or(|t| now - t >= self.config.refresh_seconds * 1000);
if personal {
self.personal_at = Some(now);
}
let all = std::mem::take(&mut self.stale) || personal;
let mut out = Vec::new();
for &uuid in on {
if self.hidden(uuid) || (!all && self.sent.contains_key(&uuid)) {
continue;
}
let lines = self.lines(uuid, &me(uuid), on.len());
if self.sent.get(&uuid) != Some(&lines) {
self.sent.insert(uuid, lines.clone());
out.push((uuid, lines));
}
}
out
}
/// Whom the Tab list goes to now: everyone each `tab_refresh_seconds`,
/// and players who just joined.
pub fn tab_due(&mut self, now: i64, on: &[u128]) -> Vec<u128> {
if !self.config.tab {
return Vec::new();
}
let all = self
.tab_at
.is_none_or(|t| now - t >= self.config.tab_refresh_seconds * 1000);
if all {
self.tab_at = Some(now);
}
let to: Vec<u128> = on
.iter()
.copied()
.filter(|u| all || !self.tab_sent.contains(u))
.collect();
self.tab_sent.extend(to.iter().copied());
to
}
/// The Tab list's header and footer.
pub fn tab_lines(&self, tps: f64, mspt: f64, players: usize) -> (String, String) {
let fill = |s: &str| {
s.replace("{server}", &self.config.server_name)
.replace("{tps}", &format!("{tps:.1}"))
.replace("{mspt}", &format!("{mspt:.1}"))
.replace("{players}", &players.to_string())
};
(fill(&self.config.tab_header), fill(&self.config.tab_footer))
}
/// `/sidebar [on|off]`: the answer, and the sidebar to show (true) or
/// take away (false). None for another command.
pub fn command(&mut self, me: &Player, line: &str) -> Option<(Vec<Out>, Option<bool>)> {
let words: Vec<&str> = line.split_whitespace().collect();
if words.first() != Some(&"sidebar") {
return None;
}
if !self.config.sidebar {
return Some((
vec![Out::Error("The sidebar is off on this server.".into())],
None,
));
}
let key = smp_store::uuid_string(me.uuid);
let show = match words[1..] {
[] => self.hidden(me.uuid),
["on"] => true,
["off"] => false,
_ => {
return Some((vec![Out::Error("Usage: /sidebar [on|off]".into())], None));
}
};
if show {
self.changed |= self.data.hidden.remove(&key);
self.sent.remove(&me.uuid);
} else {
self.changed |= self.data.hidden.insert(key);
}
let reply = if show {
"The sidebar is on. /sidebar off hides it."
} else {
"The sidebar is off. /sidebar on shows it again."
};
Some((vec![Out::Reply(reply.into())], Some(show)))
}
}
pub const COMMANDS: [&str; 1] = ["sidebar"];
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
fn dir() -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("smp-board-{}-{n}", std::process::id()))
}
fn me(uuid: u128) -> Player {
Player {
uuid,
name: format!("P{uuid}"),
level: 0,
}
}
fn accounts(n: u128) -> Vec<(u128, String, i64)> {
(1..=n)
.map(|u| (u, format!("P{u}"), u as i64 * 100))
.collect()
}
#[test]
fn amounts_are_short() {
assert_eq!(compact(0), "0");
assert_eq!(compact(1_250), "1,250");
assert_eq!(compact(99_999), "99,999");
assert_eq!(compact(125_400), "125.4k");
assert_eq!(compact(3_210_000), "3.2M");
assert_eq!(compact(7_000_000_000), "7.0B");
}
fn me_of(uuid: u128) -> Me {
Me {
name: format!("P{uuid}"),
played_seconds: 7_500,
}
}
#[test]
fn durations_are_short() {
assert_eq!(short_duration(0), "0m");
assert_eq!(short_duration(12 * 60 + 59), "12m");
assert_eq!(short_duration(7_500), "2h 05m");
}
#[test]
fn the_sidebar_shows_the_player_then_the_address_and_no_ranking() {
let d = dir();
let mut b = Board::open(&d, Config::default()).unwrap();
assert!(b.due(0, 0));
b.rebuild(accounts(12), 0, 1);
let rocky = Me {
name: "Rocky".into(),
played_seconds: 7_500,
};
let stat = |label, value: &str| Line::Stat {
label,
value: value.into(),
};
assert_eq!(
b.lines(11, &rocky, 42),
vec![
Line::Name("Rocky".into()),
stat("Balance", "1,100"),
stat("Playtime", "2h 05m"),
stat("Online", "42"),
Line::Gap,
Line::Footer("play.playhorizon.dev".into()),
]
);
// A player with no account yet; a server with no address.
assert_eq!(b.lines(99, &rocky, 1)[1], stat("Balance", "0"));
b.config.footer.clear();
assert_eq!(b.lines(99, &rocky, 1).len(), 4);
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn the_old_title_moves_to_the_new_one_and_a_set_one_stays() {
let mut old = Config {
title: "Richest players".into(),
..Config::default()
};
assert!(migrate(&mut old));
assert_eq!(old, Config::default());
assert!(!migrate(&mut old));
let mut own = Config {
title: "My server".into(),
..Config::default()
};
assert!(!migrate(&mut own));
assert_eq!(own.title, "My server");
}
#[test]
fn an_old_config_with_a_top_still_reads() {
let old = serde_json::json!({ "title": "Horizon SMP", "top": 3 });
let c: Config = serde_json::from_value(old).unwrap();
assert_eq!(c, Config::default());
}
#[test]
fn sidebars_go_out_on_join_on_a_balance_change_and_on_the_refresh() {
let d = dir();
let mut b = Board::open(&d, Config::default()).unwrap();
b.rebuild(accounts(3), 0, 1);
let asked = std::cell::Cell::new(0);
let me = |u| {
asked.set(asked.get() + 1);
me_of(u)
};
assert_eq!(b.updates(0, &[1, 2], me).len(), 2);
// Nothing new within the refresh: nobody is even looked at.
asked.set(0);
assert!(b.updates(1_000, &[1, 2], me).is_empty());
assert_eq!(asked.get(), 0);
assert!(!b.due(1_000, 1), "no change, not a minute yet");
assert!(!b.due(1_000, 2), "changed, but within min_seconds");
assert!(b.due(5_000, 2));
b.rebuild(accounts(3), 5_000, 2);
assert!(b.updates(5_000, &[1, 2], me).is_empty(), "same lines");
// Player 1 gets richer: only their own sidebar changes.
let mut a = accounts(3);
a[0].2 = 1_000;
b.rebuild(a, 10_000, 3);
let sent = b.updates(10_000, &[1, 2], me);
assert_eq!(sent.iter().map(|s| s.0).collect::<Vec<_>>(), vec![1]);
// A newcomer gets theirs at once, and only they are looked at.
asked.set(0);
assert_eq!(b.updates(11_000, &[1, 2, 3], me).len(), 1);
assert_eq!(asked.get(), 1);
// One who left gets it on return.
b.updates(12_000, &[2, 3], me);
assert_eq!(b.updates(13_000, &[1, 2, 3], me).len(), 1);
// Within the minute player 2 still shows 2 online; the minute's
// refresh mends it, and sends nothing to the others.
assert_eq!(b.updates(30_000, &[1, 2, 3], me).len(), 0);
let sent = b.updates(60_000, &[1, 2, 3], me);
assert_eq!(sent.iter().map(|s| s.0).collect::<Vec<_>>(), vec![2]);
assert!(b.due(70_000, 3), "a minute passed");
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn a_hidden_sidebar_stays_hidden_after_a_restart() {
let d = dir();
let mut b = Board::open(&d, Config::default()).unwrap();
b.rebuild(accounts(3), 0, 1);
let (out, show) = b.command(&me(1), "sidebar off").unwrap();
assert_eq!(show, Some(false));
assert!(matches!(&out[0], Out::Reply(s) if s.contains("off")));
assert_eq!(b.updates(0, &[1, 2], me_of).len(), 1, "only player 2");
b.save().unwrap();
let mut b = Board::open(&d, Config::default()).unwrap();
assert!(b.hidden(1));
b.rebuild(accounts(3), 0, 1);
assert_eq!(b.updates(0, &[1], me_of).len(), 0);
// `/sidebar` toggles it back on, and it is sent at the next update.
let (_, show) = b.command(&me(1), "sidebar").unwrap();
assert_eq!(show, Some(true));
assert_eq!(b.updates(1_000, &[1], me_of).len(), 1);
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn the_tab_list_fills_its_placeholders_and_goes_to_newcomers_at_once() {
let d = dir();
let mut b = Board::open(&d, Config::default()).unwrap();
let (h, f) = b.tab_lines(19.96, 6.3, 42);
assert_eq!(
(h.as_str(), f.as_str()),
("Horizon SMP", "TPS 20.0 · 42 online")
);
assert_eq!(b.tab_due(0, &[1, 2]), vec![1, 2]);
assert!(b.tab_due(1_000, &[1, 2]).is_empty());
assert_eq!(b.tab_due(2_000, &[1, 2, 3]), vec![3]);
assert_eq!(b.tab_due(5_000, &[1, 2, 3]), vec![1, 2, 3]);
let _ = std::fs::remove_dir_all(d);
}
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 143 files, more files were not shown because too many files have changed in this diff. Show more