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
14 changed files with 267 additions and 114 deletions

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+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"
+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."
+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."
+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."
+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."
+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."
+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."
+6 -6
View File
@@ -9,7 +9,7 @@ lines, vanilla chest menus the server fills with ordinary items (the trade menu)
bar, boss bars and the sidebar).
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, playtime and the richest players, the Tab list's
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 |
@@ -230,14 +230,14 @@ items, shearing, leads), which waits for Horizon's hook (#4648). The server's `s
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 the `board.top` (3)
richest players with their balance, then `board.footer` (`play.playhorizon.dev`, empty for none). The ranking is
built once for everyone, only when a balance 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
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 defaults (`Richest players`, the top 10) gets these at its first start of 0.14. The
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
+1 -9
View File
@@ -164,7 +164,7 @@ fn open(dir: PathBuf) -> Smp {
let mut config = config;
// Prices never edited: today's defaults (the balances are untouched).
let write = smp_market::migrate(&mut config.market) | write;
// 0.13's sidebar (the richest ten), never edited: 0.14's scoreboard.
// 0.13's sidebar title, never edited: today's.
let write = smp_board::migrate(&mut config.board) | write;
if write && let Err(e) = smp_store::write_json(&config_path, &config) {
log::error!("[smp] {e}");
@@ -2402,14 +2402,6 @@ fn sidebar(title: &str, lines: &[Line]) -> NativeSidebar {
)
}
Line::Gap => (Text::literal(""), None),
Line::Heading => (Text::literal("Richest").with_color(text::YELLOW), None),
Line::Top { rank, name, amount, me } => {
let color = if *me { text::GREEN } else { text::WHITE };
(
Text::literal(format!("{rank}. ")).with_color(text::GRAY).append(Text::literal(name.clone()).with_color(color)),
Some(Text::literal(amount.clone()).with_color(text::GOLD)),
)
}
Line::Footer(address) => (Text::literal(address.clone()).with_color(text::AQUA), None),
})
.collect();
+36 -89
View File
@@ -1,10 +1,10 @@
// The SMP's screens: a scoreboard sidebar, the player's own block first (their
// name or nickname, balance, playtime, the players on), then the three
// richest and 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 ranking is
// built once for everyone when a balance changed (at most once each
// `min_seconds`) or every `refresh_seconds`; the players' own lines are looked
// 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`;
@@ -16,10 +16,6 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Path, PathBuf};
/// The most of the richest a sidebar lists: vanilla's 15 lines, less the
/// player's four, the two gaps, the heading and the address.
const MAX_TOP: usize = 7;
/// `config.json`'s `board` section.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
@@ -27,11 +23,9 @@ pub struct Config {
/// The sidebar (each player may hide it).
pub sidebar: bool,
pub title: String,
/// How many of the richest players it lists (at most 7; 0: none).
pub top: usize,
/// Its last line, the server's address ("": none).
pub footer: String,
/// The ranking is built again, and the players' own lines looked at,
/// 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.
@@ -50,7 +44,6 @@ impl Default for Config {
Self {
sidebar: true,
title: "Horizon SMP".into(),
top: 3,
footer: "play.playhorizon.dev".into(),
refresh_seconds: 60,
min_seconds: 5,
@@ -63,19 +56,14 @@ impl Default for Config {
}
}
/// SMP 0.13's sidebar defaults (the richest ten), moved to 0.14's when a
/// server never changed them. True when something changed.
/// 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 {
let mut changed = false;
if config.title == "Richest players" {
config.title = Config::default().title;
changed = true;
return true;
}
if config.top == 10 {
config.top = Config::default().top;
changed = true;
}
changed
false
}
/// `board.json`.
@@ -95,16 +83,6 @@ pub enum Line {
Stat { label: &'static str, value: String },
/// An empty line.
Gap,
/// The heading over the richest.
Heading,
/// One of the richest: their rank, name and balance; `me` on the
/// player's own line.
Top {
rank: usize,
name: String,
amount: String,
me: bool,
},
/// The server's address.
Footer(String),
}
@@ -131,10 +109,9 @@ pub struct Board {
path: PathBuf,
data: Data,
changed: bool,
/// Every account by balance, richest first: UUID, name, balance.
ranked: Vec<(u128, String, i64)>,
rank_of: HashMap<u128, usize>,
/// When the ranking was built, and over which economy revision.
/// 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.
@@ -180,8 +157,7 @@ impl Board {
path,
data,
changed: false,
ranked: Vec::new(),
rank_of: HashMap::new(),
balances: HashMap::new(),
built_at: None,
built_revision: 0,
sent: HashMap::new(),
@@ -204,7 +180,7 @@ impl Board {
self.data.hidden.contains(&smp_store::uuid_string(uuid))
}
/// Whether the ranking should be built again: the economy changed
/// 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 {
@@ -218,27 +194,17 @@ impl Board {
|| since >= self.config.refresh_seconds * 1000
}
/// The ranking from the accounts (UUID, name, balance, any order).
pub fn rebuild(&mut self, mut accounts: Vec<(u128, String, i64)>, now: i64, revision: u64) {
accounts.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.1.cmp(&b.1)));
self.rank_of = accounts
.iter()
.enumerate()
.map(|(i, a)| (a.0, i + 1))
.collect();
self.ranked = accounts;
/// 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, the richest from the ranking, the
/// address.
/// `uuid`'s sidebar: their own block, then the address.
pub fn lines(&self, uuid: u128, me: &Me, online: usize) -> Vec<Line> {
let balance = self
.rank_of
.get(&uuid)
.map_or(0, |&rank| self.ranked[rank - 1].2);
let balance = self.balances.get(&uuid).copied().unwrap_or(0);
let mut lines = vec![
Line::Name(me.name.clone()),
Line::Stat {
@@ -254,19 +220,6 @@ impl Board {
value: online.to_string(),
},
];
let top = self.config.top.min(MAX_TOP);
if top > 0 && !self.ranked.is_empty() {
lines.push(Line::Gap);
lines.push(Line::Heading);
lines.extend(self.ranked.iter().take(top).enumerate().map(
|(i, (u, name, balance))| Line::Top {
rank: i + 1,
name: name.clone(),
amount: compact(*balance),
me: *u == uuid,
},
));
}
if !self.config.footer.is_empty() {
lines.push(Line::Gap);
lines.push(Line::Footer(self.config.footer.clone()));
@@ -429,7 +382,7 @@ mod tests {
}
#[test]
fn the_sidebar_shows_the_player_then_the_three_richest_then_the_address() {
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));
@@ -442,12 +395,6 @@ mod tests {
label,
value: value.into(),
};
let top = |rank, name: &str, amount: &str, me| Line::Top {
rank,
name: name.into(),
amount: amount.into(),
me,
};
assert_eq!(
b.lines(11, &rocky, 42),
vec![
@@ -456,27 +403,20 @@ mod tests {
stat("Playtime", "2h 05m"),
stat("Online", "42"),
Line::Gap,
Line::Heading,
top(1, "P12", "1,200", false),
top(2, "P11", "1,100", true),
top(3, "P10", "1,000", false),
Line::Gap,
Line::Footer("play.playhorizon.dev".into()),
]
);
// A player with no account yet; a server with no address and no top.
// 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();
b.config.top = 0;
assert_eq!(b.lines(99, &rocky, 1).len(), 4);
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn the_old_defaults_move_to_the_new_ones_and_set_ones_stay() {
fn the_old_title_moves_to_the_new_one_and_a_set_one_stays() {
let mut old = Config {
title: "Richest players".into(),
top: 10,
..Config::default()
};
assert!(migrate(&mut old));
@@ -484,15 +424,21 @@ mod tests {
assert!(!migrate(&mut old));
let mut own = Config {
title: "My server".into(),
top: 5,
..Config::default()
};
assert!(!migrate(&mut own));
assert_eq!((own.title.as_str(), own.top), ("My server", 5));
assert_eq!(own.title, "My server");
}
#[test]
fn sidebars_go_out_on_join_on_a_new_ranking_and_on_the_refresh() {
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);
@@ -511,11 +457,12 @@ mod tests {
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: everyone's top changes.
// 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);
assert_eq!(b.updates(10_000, &[1, 2], me).len(), 2);
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);
+3 -4
View File
@@ -452,7 +452,7 @@ def world_guard(server, alex, steve):
def board(alex, steve, out):
"""The scoreboard sidebar (the player's own block, the richest, the address) and the Tab list; /sidebar off
"""The scoreboard sidebar (the player's own block, then the address) and the Tab list; /sidebar off
hides it, on shows it again."""
r = {}
side = lambda c: c.run("status").get("sidebar")
@@ -462,9 +462,8 @@ def board(alex, steve, out):
break
time.sleep(0.5)
assert r["sidebar"]["objective"] == "horizon:native_sidebar" and r["sidebar"]["title"] == "Horizon SMP", r
# Steve's name, balance, playtime and the players on; a gap, "Richest", Alex and Steve (the two accounts); a gap,
# the address.
assert len(r["sidebar"]["scores"]) == 10, r
# Steve's name, balance, playtime and the players on; a gap, the address. No ranking of the richest.
assert len(r["sidebar"]["scores"]) == 6, r
# Holders starting with '#' would be hidden from the sidebar (only the title would show).
assert not any(o.startswith("#") for o in r["sidebar"]["scores"]), r
steve.run("key", "tab", "down")