Files
horizon-mods/native/smp
d0d3e68ec4 SMP: the /vanish reminder sits on the action bar, not mid-screen (#298)
rdbt (operator, #mod-chat 11-05): the centred 'Vanished: only operators see
you' line covers the view. It is now an action bar line refreshed once a
second; unvanishing sends an empty action bar instead of clearing titles.
proof.py checks the action bar and that nothing shows mid-screen.

Co-authored-by: Horizon agent <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-11-05 18:38:01 +01:00
..

SMP (Horizon native server mod)

The official Horizon server's survival layer (design). It is not part of Horizon's own server: only a server that builds this mod into its engine has it. It isn't listed in the registry.

Players install nothing: it speaks only what a stock Horizon client already shows (commands with completions, chat 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 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 <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)
/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)
/trade accept [player], /trade deny [player] answers a request
/trade money <amount> the money you add to your offer (0: none)
/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
/rtp, /wild to a random safe spot 500 to 10,000 blocks from the spawn, in the Overworld only; once every 5 minutes
/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
/pingsound, /pingsound on, /pingsound off whether you hear a ding when someone names you in chat or /msgs you; turns it on or off
/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
/vanish, /vanish list operators of level 3: hides you from the other players, or shows you again; who is vanished
/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
/smp status, /smp off [part|all] [reason], /smp on [part|all] operators of level 3: the emergency switches (below)
/report <player> <reason> anyone: tells the staff about a player (a reason is needed; one report every reports.cooldown_seconds, 120). Operators of level 3 on read it at once; one who joins later is told how many came in the last 24 hours. Kept in reports.log and the server's log
/reports [page] operators of level 3: the reports, newest first (the last 100 kept in memory, read back from reports.log at the start)
/ledger [page], /ledger <player> [page], /ledger minted operators of level 3: the coin ledger (audit.log): the newest coin movements (who to whom, how much, why, when), one player's, or the coins the server made and destroyed in each of the last 6 hours and what made most of them. Reads the end of the file (1 MB) only when asked

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:

  • 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.2 times its price, it at 1.6) too.
  • There are no daily coin or per-item limits on market sales, or daily limits on /pay receipts. The former daily_sell_coins and daily_sell_items settings and market.json counts are ignored, including in existing server files.
  • 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).

Vanish. /vanish takes an operator of level 3 out of the Tab list and the world of every player below level 3, whatever the game mode (survival and creative too), and those players don't read them joining or leaving; operators of level 3 or more still see them, and are told who vanished. A vanished operator reads a line above the hotbar the whole time (Vanished: only operators see you), isn't counted online (the sidebar, the Tab list's footer), doesn't hold the night up, isn't announced as AFK or on a first join, and can't be reached by name by those who don't see them (/tpa, /trade, /coinflip, /playtime). It lasts over a relog and a restart (vanish.json) until /vanish again. Needs horizon#7003 (native_hide_player).

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 50: a diamond moves 25 times faster per item than cobblestone, and any item reaches the floor within 10,000 sold; 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.2) and max_factor (1.6), where the rest of a trade goes at that price. With no daily sale caps (since 0.33), the floor is what bounds a farm: selling one item without end takes its price down to a fifth of its base, where it stays while the selling goes on; a config with the old floor (0.6) gets 0.2 when it loads. 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."). Market purchases can be sold back immediately; the former round_trip_hours setting and saved purchase receipts are ignored. (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 , 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. It goes through the market's own rules (survival or adventure only, items then money, logged); 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 moves nothing: offered stacks stay in their owners' inventories, the menu shows copies, and a stack that changes (used, dropped, grown by a pickup) leaves the offer. Any change clears both confirmations. When both have confirmed, the server checks that each still holds exactly what they offer, has room for what they get and has the money, logs the trade, and swaps everything in one step, or nothing (and both must confirm again). Closing the menu, /trade cancel, leaving, dying or the other player leaving ends the trade with nothing moved. Survival and 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). 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 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.

/rtp (or /wild) draws rtp_attempts (6) chunks at random between rtp_min_distance (500) and rtp_max_distance (10000) blocks from the spawn (/setspawn's point, 0 0 without one), uniform over the ring's area and at least 16 blocks inside the world border (as last saved in world_border.dat), then holds them loaded one after another with vanilla's forceload while the warmup runs: the game generates them on its worker threads, never on the server tick. Once a chunk is loaded, the Overworld's tick reads its 256 columns (native_read_columns, the top block below the MOTION_BLOCKING_NO_LEAVES height and the two above it) and the player lands, still standing still, on a random one whose top block is solid ground (no fluid, fire, cactus, magma, powder snow, berry bush or dripstone) with air or grass above it, outside another player's claim and still in the ring and the border; with none the next chunk is tried. With no spot after the six, or no land loaded rtp_search_seconds (30) after the warmup, it says so and nothing is spent. A teleport made starts rtp_cooldown_seconds (300) for that player, kept in places.json over restarts ("Wait 2 min before your next /rtp."); a teleport called off (a move, a hit, the game failing it) doesn't. Elsewhere than the Overworld it refuses. The chunks it forced are let go 10 s after the landing, or at the next start if the server stopped first; a chunk an operator forced already is used and left forced. /back returns from it as from any teleport.

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 (a TNT cannon, a creeper, a crystal, a bed, a wither skull), nor the claim's animals, paintings and item frames. Buckets are guarded where they fill or empty; nothing is placed against a claim's border from outside (a block, fire, a spawn egg, an armour stand); a dispenser does nothing to the block in front of it in another's claim; lava outside a claim lights none of its blocks (lava 4 or more blocks inside still lights its owner's). A projectile of someone not trusted (an arrow, a trident, a snowball, a pearl, a potion, a fire charge) does nothing in a claim but to monsters and players: no button pressed, target lit, chorus or pot broken, TNT lit, crystal set off, pearl landed or animal hurt; a dispenser's counts as from the land it shot from, a mob's hurts animals only. Hitting or right clicking the claim's item frames, armour stands, paintings, boats, minecarts and animals (leads, saddles, shears, dyes, buckets, a frame's or a stand's items) does nothing for others (claims.protected_entities, entity ids; monsters and players aren't in it; villagers and wandering traders still trade; end crystals are always guarded, even when a world's list predates them). Not guarded yet, for want of a Horizon hook: wind charges bursting outside a claim (they open doors inside), armour stands and vehicles in a blast, farmland trampled, a wither's or an enderman's own block changes. The server's spawn-protection (vanilla's) guards the spawn.

Chat pings. A chat line that names an online player (a whole word, any case: alex, @Alex) plays the experience orb pickup sound to that player alone, and so does a /msg, /tell or /w to them; never the sender, at most once each 3 s per player. /pingsound off turns it off for oneself (pings.json), /pingsound on back.

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).

Emergency switches. An operator of level 3 turns parts of the SMP off while the server runs: /smp off (everything but land protection), /smp off <part> [reason], /smp on [part], /smp status (what is off, by whom, when and why). Parts: economy (balances, /pay, /eco, the daily reward), market (/worth, /sell, /buy, /shop, /market), auction, trades, gambling (coinflips, the lottery, the casino), places (spawn, homes, warps, /tpa, /back), claims (land protection and /claim: only by name, /smp off claims), board (the sidebar and the Tab list's header and footer), presence (welcomes, AFK, the commands' rate limit), nicks, invsee. A part that is off answers its commands with "... is turned off for now by the server's staff", and its menus close at the next click. Turning one off ends open trades with nothing moved, closes the shop, market and auction menus, calls off teleports waiting, takes the sidebars and nicknames away. Its timed work waits (the lottery's draw, auctions ending, the daily reward); nothing held is lost: coinflips not taken still go back and a casino round under way still ends and is paid. The other operators on are told, and each change is in the server's log and audit.log (switch_off, switch_on). What is off is kept in switches.json, so a restart keeps it off.

Data

Everything is in the world's folder, world/horizon-smp/, so a backup of the world carries it. A world without that folder is untouched until the mod first runs on it; it changes no block and no player.

File Holds
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), 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
vanish.json the vanished operators by UUID, with their name
reports.log one JSON line per /report: time, by/by_name, player/player_name, reason, where each stood (by_at, player_at when on)
staff.log one line per /invsee or /endersee menu in which something moved: who, whose, what was taken and put
market_prices.json each item's price factor and history (minutes for 24 h, hours for 30 days: [time, mid, traded, sold]), 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)
switches.json the parts operators turned off (/smp off): by whom, when and why
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
pings.json the players who turned chat pings off (/pingsound off)
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, flow (minted or burned) for money the server made or destroyed (a market sale or purchase, the daily reward, /eco give and take); 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 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 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), native_hide_player (API 314, horizon#7003), the server tick and the world lifecycle. compatibility.json names "hooks": 58.

File What it is
mixins/smp/horizon-mixin.toml The bundle: twenty-nine 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, smp_switch.rs, smp_vanish.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, game::smp_switch, game::smp_vanish; 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,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

(cd native/smp && cargo test)                          # the rules
python3 native/tools/native.py check smp               # apply on the tested Horizon and cargo check the game
python3 native/tools/native.py proof smp --resources /path/to/.local/horizon-client/26.3

The official server's engine

The server runs horizon-server as usual, with [engine] path pointing at a game built with this mod:

NATIVE_NO_DEFAULT_FEATURES=1 python3 native/tools/native.py build smp --horizon <the release's commit>

prints the built horizon-client (--no-default-features: the server build, without audio and controllers). When to deploy it on the official server is the owners' decision.