Everything we published

Beta

STYGION Keystone b0.1.2: what's new

Keystone's commands survive a /reload. Reloading data packs gives the game a new command tree, and Keystone's commands were not in it until the next restart; they are put back as soon as the reload lands, and every player is sent the new tree.

Security, from the review of 8 October 2026.

  • Votes are worth one vote. One vote per site per person per voting day, and no more in a day than /vote lists sites (votes.limit.per-day, 0 = that many); the whole port takes at most votes.limit.per-minute (30) a minute. A vote counts as when it arrived here, never as the sender dated it, and one dated more than a day from this server's clock is refused — the original protocol is encrypted with a key every vote site holds, so its dates and site names are whatever the sender liked. A streak reward is paid once on its day, not once per vote that day. votes.accept-v1 = false takes only signed v2 votes. A sender that trickles bytes is cut off after ten seconds, and no more than 32 connections are open at once.
  • What can make somebody an operator is the owner's. Settings, groups, perks, restoring a backup, running operations and the assistant's writes need a key issued to the console or a level-4 operator — checked on every request, whatever wildcard a key holds. A key from before owners were recorded is not the owner's until the owner says so with /keystone key own <id>; the log names each one at boot. A key somebody named like one of the mod's own is never widened into it. Applying an assistant's proposal in game, and revoking the owner's key, are the owner's too. A panel link for somebody who reached it through a group is read-and-moderate only. /keystone key new hands out nothing its maker could not be handed. Vote rewards, streaks, party rewards and crate loot (which run as the console) can be changed in game only by the owner. "Owner" means operator level 4, as everywhere else, not any operator level.
  • /execute as changes who a command is about, never who is asking. Permissions and "the owner" come from whoever really typed the command: their own level and their own groups, never those of the player execute as named (somebody at op 2 could run Keystone's commands as the owner and be the owner). A command block or a function is level 2 with no groups, whoever it runs as, and is never the owner; the console and RCON are. A level-4 operator running a command as somebody else may use it but is not the owner for it. Logs name both: Alice as Bob.
  • A vote pays a person, not a name. On a server in offline mode a name costs nothing, so a vote is always written down but pays only somebody who has played here for votes.pay.played (30 minutes) — a name nobody has joined as is never paid — and, with votes.pay.discord, whose account is linked to Discord. Linked accounts count as their Discord account: several names on one Discord share one voting day and one streak reward a day. Waiting votes pay as soon as the conditions are met, and the player is told on joining what is missing. The per-minute limit is per vote site and counts only votes that pass the limits, so one noisy sender no longer shuts out the honest ones.
  • A linking code is no longer handed to whoever types a name that has played here. With link.required-to-join on an offline-mode server, the door used to give a code to anybody under any unlinked name — and spending it on Discord made them that player, with the save, the rank and the statistics behind it, whatever password the real owner had. Now a name that has a world save or statistics, a rank, a row in Keystone's records or a password here gets no code at the door. With a password (and accounts.require on) they are let in, held, and leave with their code on the screen once /login proves it; without one they read discord.screens.name-has-history and open a ticket, and /discord permit <name> (keystone.discord.permit) lets that name get one code — used up by the link it makes, lapsing after a day. A name nobody has worn here gets its code as before; online-mode servers are unchanged. The permit is for the name exactly as written — capitals count, because on an offline-mode server Steve and steve are two people with two saves.
  • A name linked to Discord can no longer be taken with /register. With accounts.require and ask-on-discord on, somebody joining under a linked name that had no password was held while its owner was asked on Discord — and could type /register x x and walk in without any answer. The same happened when no question could be sent (one already waiting, the bot down): they were offered /register. Now a linked name sets a password only after confirming on Discord, and when it cannot be asked it is disconnected. New setting accounts.passwords (on by default): off removes /register, /login and /password altogether, so on a server where everybody links Discord the only proof is confirming the join there.
  • /keystone key list marks the owner's keys and says when each was made.
  • The local API reads nobody before it knows them, and at most a megabyte from anybody; MCP tool calls go through the same rate limit, owner check and audit record as every other request.

The editor, redrawn — and it shows the whole server now. /keystone panel opens a new editor in STYGION's own colours, light or dark with the system (or pinned by a switch), with everything grouped in a rail: an overview (players, TPS, tick and memory now and over the last day, what is coming up, and what needs somebody — open tickets, the assistant's proposals, a staged restore, a module that did not start), players (look anybody up; what they hold and where from; put them in a group for a while, give a perk, ask whether they may do something; progress, votes and crate keys; leaderboards and the rank ladder), groups with every rule they end up with and where it came from, bans and accounts, operations (run now or in 1, 5 or 15 minutes, call off, maintenance with a reason, what a clean would take), backups (and putting one back, staged for the next boot and cancellable), performance (live, a week of history, load tests), console and chat (levels, search, copy), votes, tickets, crates, Discord, settings, the change history, the assistant and keys and the audit log. Ctrl K (or /) finds any screen, setting, player or group and runs the common actions. A screen only appears when its module runs.

Settings are drafts until you apply them. Change as many as you like; each is checked by the server as you type and refused in words if it does not fit. Ctrl S shows every change beside what it is now, and applies the ones the server took — each a version the history can put back, with an Undo right there. Drafts survive a closed tab (secrets never leave the page). Flags are switches, lists are chips, times of day are a clock, ids are picked from what the server loaded, and chat text is drawn the way a vanilla client draws it.

API. POST /v1/settings takes check=true to ask whether a value would be accepted without writing it. GET /v1/settings and the settings history answer a list as a, b rather than [a, b], which could not be written back. GET /v1/backups answered 500 whenever no restore was staged — the usual case — and now answers; so did GET /v1/gate whenever anybody had been let in by name. The tick time in /v1/metrics is what a tick costs now, from the server's own tick times — it was 1000 ÷ TPS, the gap between ticks, so every healthy server read 50 ms "of the 50 a tick allows".

Crates: a key for every day you play. A new module, crates, off until an owner turns it on (enabled = true in config/keystone/crates.toml) — a module that hands things out is not something an update should switch on by itself, so a module can now say it starts off (ModuleProvider.onByDefault). Everybody gets one key on each day they are on the server — on their first join of the day, or at midnight if they are online when it passes — counted in the server's time zone (crates.time-zone). Keys live in Keystone's database, not in an inventory, so they cannot be traded, dropped or duplicated; they are earned, never sold. /crate shows how many you hold and your odds, /crate open spends one, /crate odds prints every rank's odds and what each rarity holds. Admins have /keystone crate give <player> <n> (works for somebody offline, and from a vote: keystone crate give {player} 1 in votes.rewards), /keystone crate history <player> and /keystone crate test <rank> [rolls], which rolls up to a million crates without giving anything and prints what came out beside what was expected.

A better rank can improve the odds of a rarer pull, and nothing else. crates.odds is one line per group, worst first — default: 70, 22, 7, 1, vip: 66, 24, 8.5, 1.5, vip-plus: 62, 25, 10.5, 2.5, legend: 57, 26, 13, 4 — and a player rolls with the last line whose group they are in. A server that writes only default gives everybody the same odds. Players can read all of it. What a rarity holds (crates.loot.common … .legendary, empty until the owner writes them) is the same for everybody, in the same shapes as a vote reward — a command, perk, rank — plus item <id> <n>, a weight in front (3: …) and a label after |. A rarity with nothing in it cannot come up, and the odds shown say so.

Items a server describes, with a look its own resource pack draws. crates.items names them: <id> <item> [model=<n>] [look=<ns:path>] [colour=#rrggbb] [glint] <name> | <lore>. model= is the number a pack matches on Minecraft 1.21.1, look= the model it names from 1.21.4, and with a look the name and lore are also read from the pack in the player's language. Such an item stacks to 64 and does not glint unless asked.

An item with a look is protected. It carries Keystone's mark in its custom data, which no survival action can write. An anvil offers no result, and lets nothing be taken, while either slot holds one — at every return of the anvil's own logic, including the one NeoForge's AnvilUpdateEvent takes; a creative player below operator level 2 cannot write one into a slot (the creative inventory is the one place a client sends whole items); and one that enters an inventory loses a name an anvil gave it elsewhere. So a server can treat it as genuine.

Whatever does not fit in an inventory is dropped at the player's feet, where only they can pick it up — the way /give does it. A crate opened in the tick before somebody left is handed over on their next join. A legendary is announced to the server (crates.announce).

Colour on a client with nothing installed. One reader for what an owner writes about colour — the sixteen & colours and five styles, &#rrggbb for any colour, &r, && for an ampersand — used everywhere Keystone draws: chat lines, announcements, titles, the countdown bar, the tab list, kick and ban screens and the refusals at the door. Until now only the tab list read &, and everything else printed &6 as two characters. Text somebody else wrote — a Discord message, a ticket — is printed as written, so &4[ADMIN] typed on Discord does not arrive red. The rank goes in front of the name in chat: a group's prefix and suffix are put on the player's display name, which a vanilla client draws and which is not part of a signed message; where a scoreboard team already prefixes the name, or FTB Ranks is installed, Keystone leaves the name alone rather than doubling it.

A player's words can never look like the server's. Colour codes and language parts are read only in text the owner wrote — settings, language files, rank prefixes (Message.formatted in the API, new). Everything else — a name, a ban reason, a Discord message, a ticket, an assistant's answer, any Message.literal — is printed exactly as written: no colour, no line break, no [xx] language part. Nothing in text can make a clickable or hover component; only code builds those.

Each player picks their language. A server that adds a language file to config/keystone/lang/ (naming itself with language.name=) asks every player once, with a line to click, which language Keystone should speak to them; /language changes it later. The choice is kept per player; until somebody chooses, their game's language is used where the server offers it, and language.default where it does not. language.offered narrows which languages players may choose — ["cs"] makes a server Czech only, and nobody is asked. English stays the floor for any line a translation lacks. The jar itself still ships English only.

Translate in the editor. A Languages screen lists every language the server has, how much of it is translated and which are offered; opens one with every line beside the English, a filter for what is still untranslated, the colours drawn as the game draws them and a placeholder the English does not have marked (and refused when saving); starts a new one from a code and its name; and exports and imports the .properties file. Saving writes config/keystone/lang/<code>.properties and players read it at once. Writing needs the owner's link — /keystone panel from an operator or the console; a moderator who reaches it through a group gets a link without languages.write.

An owner's own text in more than one language. Anything written in a setting that a player reads — announcements, the kick, ban, full and maintenance screens, the tab list — may carry a part per language: [en]The server is full. [cs]Server je plný. Each player reads their part, then the server's language.default, then English; text with no marker reads as it always did.

A fresh server lets players use their own commands. The default group used to start empty, so an ordinary player could not use /vote, /playtime, /skin, /discord or /crate until an owner found out from a player. It now starts with those; anything an owner already decided about them is left alone.

Discord. discord.status-channel says that the server is up, is going down, or will restart in so many minutes (once per restart, not at every warning) — by default ("chat") in the chat channel, where the players are; a channel id sends it elsewhere and blank nowhere. news-channel is no longer read: a news channel is for news. The list of channels and roles is read at most once per half minute, however many of them change — every channel edit, including the bridge's own topic, used to spend Discord's ten-a-minute budget shared with everything else on the same bot.

Keystone's own failures can go to an error tracker — Sentry or GlitchTip, from errors.dsn in keystone.toml. Off by default, and the jar carries no address: nothing leaves a server unless its owner writes one. Only failures with Keystone's code in their stack are sent, with the exception type, its frames, the version and the loader, and the exception's message only after every secret the server's settings hold, tokens and keys, addresses, e-mails, URLs, file paths, UUIDs and anything quoted are removed — no player names, addresses or chat; the same failure at most once in ten minutes, never more than thirty a minute. Written without the Sentry SDK, whose global client another mod on the same server could take over.

A leftover setting is cleaned up. 0.1.0 wrote announce() into progress.toml beside the real announce; any key whose name ends in () is now removed when a settings file is read.

Everything new is a setting, and takes effect without a restart. territory.fly.* and territory.bonus are read on every pass (a bonus line broken by an edit keeps the last table that read); the two limits on flying anywhere are settings too — fly.nether-roof (128, 0 lifts it) and fly.over-foreign-claims (off). keystone.toml gains language.ask-on-join, chat.rank-in-name and chat.step-aside. crates.time-zone and discord.news-channel are read when used, the news lines are language keys (discord.news.up, .down, .restart) a server can reword, and discord.names-every sets how often channels are re-read. /keystone reload now also re-reads the server's language files and errors.*.

Restarts. The scheduled restart came back to nothing on a server started where it lives: the game reports that directory as "", and a process told to start in "" does not start, so the restart refused itself every night. It is made absolute now. /keystone operations restart by hand no longer skips a healthy server as "nothing to gain" — uptime, memory and TPS are reasons for the clock, not conditions on an operator — while every other operation keeps all its checks when run by hand, and one that throws or gives no answer does not run. restart.after-uptime, .above-memory-percent, .below-tps and .wait-for-empty now take effect when they are changed, as /keystone set always said they did.

VIP works on the land FTB Chunks already keeps. A new module, territory (config/keystone/territory.toml), reads claims and teams from FTB Chunks and FTB Teams when they are installed — found at start, never required, and on a server without them it says so once and stays quiet. Nothing of Keystone's own claims is needed.

  • /fly over your own land. Somebody in a group named in fly.groups (vip-plus and legend by default), holding keystone.territory.fly, or an operator, turns it on with /fly and off the same way. It works only in chunks their own FTB team claimed — in every dimension — and switches off at the edge with the fall caught (slow falling until they land); it comes back by itself over their land. Allies count only with fly.allies = true. Creative and spectator are left alone, and flight another mod gave (a jetpack, a ring) is never taken away. A rank that ends grounds them on the next pass, even if it ended while they were offline.
  • More chunks per rank, and for time played. bonus maps a paid rank to extra claimed and force-loaded chunks (vip=25/1, vip-plus=50/2, legend=100/3 by default) and earned does the same for playtime ranks (wanderer=5/0 …, empty by default); the two layers add up, and inside each the best rank counts. Written into FTB Chunks' own limits so its claim screen shows the bigger number. It is applied when a rank starts, renews or ends and at every join, and Keystone moves only its own part of the number — extra chunks an admin gave by hand stay. A bonus line that does not read keeps the module off and the log says which.
  • Flight anywhere for some ranks. fly.anywhere-groups (empty by default, or keystone.territory.fly.anywhere) lets a rank fly outside its own land too — everywhere except a claim of another team that does not count it as an ally, and the nether roof from y 128 up.
  • /keystone territory (keystone.territory.admin) says what land is read from; check <player> shows where they stand, their team and their extras; sync <player> applies the bonus now.

/spawn, warps and /vanish. A new module, essentials. /spawn and /warp <name> (Tab offers the names; on its own, or /warps, it lists them) for everybody — keystone.spawn and keystone.warp, which the default group now starts with. /setwarp <name> makes or moves a warp where you stand (keystone.warp.set), /delwarp removes one (keystone.warp.delete), and the editor's Warps screen lists them and deletes them. A warp into a world that is not loaded is listed as broken and refused with the reason. /vanish (keystone.vanish) takes staff out of every other player's world and tab list, out of the server list's count and names, the query port, /list, the join and leave lines, the Discord bridge's join line and topic, and the counts the local API gives out; mobs ignore them and, unless vanish.pick-up-items, items stay on the ground. Whoever holds keystone.vanish.see still sees them. It survives a reconnect and a restart. Each part can be switched off in essentials.toml; where another mod already has /warp or /spawn, Keystone's is under /keystone.

Who may rewrite what players read. /keystone panel gives the console and operators a link to everything; anybody else who reaches it through a group gets every scope but languages.write — and is refused, rather than handed everything, when the list of scopes is not ready. Keys remember whether they were issued to the owner, and writing a translation asks that as well as the scope (a key made with keystone key new … * by a non-operator does not pass).

A module that is off still shows its settings. The editor and the file used to show only enabled for it, so crates could not be set up before the day they went on. Its settings are listed now, under a note that it is off (operations, moderation, skins, the tab list and the assistant still show only enabled while off).

A command another mod already owns stays theirs. Keystone used to register its roots over whatever was there, which handed the other mod's command to Keystone under the other mod's permission check. It now leaves the root alone and says so in the log.

A kick shows its reason. The game closed the connection the instant the disconnect packet left, with whatever the client had just sent still unread — and Linux answers that by resetting the connection and throwing the packet away, so a kicked player read Connection reset by peer instead of why. Every kick, ban, maintenance close, restart and failed sign-in now reads the client dry, sends the reason, then closes the sending half so the reason arrives first. A Discord ban no longer disconnects somebody off the server thread.

Op is op. An operator (level 4, the level of /op and /stop) may use everything; groups add on top and never take anything away from one. The command tree and Player.has now ask the same rule — before, an operator could be told they had a permission and then be refused the command. Operators below level 4 are not owners here, on purpose: an owner who set op-permission-level=2 limited them. A command block no longer passes just for not being a player.

/kick and /ban could be used by anybody. With take-over-vanilla on (the default), the game's own /kick, /ban, /pardon and /banlist were done Keystone's way before the game checked who typed them. They now need keystone.kick / keystone.ban like /keystone kick and /keystone ban. And a moderator who is not op cannot kick or ban an operator, or anybody in a heavier group than their own.

Confirming on Discord is remembered like a password — same remember-for, same exact address (a keyed hash, never the address itself). 0.1.0 asked a Discord-only player on every join and called it deliberate; it was a bug. A remembered sign-in ends when the Discord link is removed or moved, on a ban, a password change, keystone accounts forget, or "that is not me". An address two accounts have proved themselves from — a shared house, every player behind a proxy — is never remembered.

Until somebody has proved who they are, they hold nothing. On an offline-mode server a name is all an operator's identity is, so a held player now has no operator rights, no group and no permission, and the game drops every packet from them except the few that keep the connection up and carry /login: no chat, no blocks, no inventories, no creative menu.

Guessing passwords costs time, and cannot lock the owner out. Past most-tries wrong passwords on a name from one place, each try waits twice as long (30 seconds up to an hour). A name has a budget of ten wrong passwords an hour from places it has never been used from, and then passwords from those places rest for the hour; the owner's usual place is not held to it, and the question on Discord is never held back by any of it. A place guessing at many names is slowed, never locked. Somebody joining as you over and over no longer pings you every time: one question a minute and five an hour from one place.

Descriptions say what ships. The mod's own description no longer promises quests, teams and claims.

A rank can be taken back over the API, and given to somebody who is away. POST /v1/players/ungroup (permissions.write, fields player and group) removes only the group grants the calling key made — what an admin typed, what Discord granted and what another key gave stay — and answers {player, group, removed}; removed: 0 is not an error. /v1/players/group and /v1/players/ungroup now find a player who is not online but has joined this server before, because ranks are bought and run out while people are away. A name this server has never seen, or a group that does not exist, is a 400.