mirror of
https://github.com/horizon-rust/horizon-mods.git
synced 2026-11-07 11:05:58 -10:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82e29462a9 | ||
|
|
725ff7a410 | ||
|
|
64b5237f7c | ||
|
|
7d17c4d63a | ||
|
|
aab9aa4c22 | ||
|
|
6d9a7492e2 |
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
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user