# Ranks, votes and the tab list

> Promotions that happen on their own, vote rewards that wait for the player, and a tab list that says who is who.

Source: https://stygion.eu/docs/keystone/ranks-and-votes
Updated: 2026-10-10

## Ranks earned by playing

`progress.toml`. `promotions` is the ladder: how long somebody has to have
played, and what they get when they get there. Keystone checks every
`check-every` and writes progress down every `write-every`, so a crash costs at
most half a minute of somebody's evening.

- `count-idle-time` decides whether standing in a corner counts. Off, and
  `idle-after` (default five minutes) is when somebody stops counting as
  playing.
- `announce` says it out loud when somebody moves up. This is the whole point of
  a rank; a promotion nobody sees is a database row.

`/playtime` says how long somebody has played; `/playtime <player>` asks about
somebody else, behind `keystone.playtime.others`.

Nobody has to run anything weekly. That is the feature.

## Votes

`votes.toml`. Keystone listens for the vote itself — `address` and `port`
(default 8192) are where the vote sites reach it, and `sites` is which ones you
are listed on. `/keystone votes keys` prints what a site needs from you.

**`address` is empty by default and nothing listens until you set it.** That is
deliberate: opening a port is a decision.

- `rewards` is what a vote is worth. **A reward waits for a player who was
  offline** rather than evaporating, which is the thing most setups get wrong. A
  reward can be a crate key too: `keystone crate give {player} 1`.
- `streaks` rewards voting several days running. A streak reward is paid once on
  its day, not once per vote that day.
- `party-goal`, `party-window` and `party-rewards` are the whole-server version:
  reach so many votes inside a window and everybody gets something.
- `announce` and `announce-cooldown` say it in chat without turning a busy
  evening into a wall.
- `time-zone` decides when a day rolls over, which is what a streak counts.
- `services` lists the site names a vote may carry, exactly as the log shows
  them (`Ada voted on Czech-Craft.eu`). A vote naming any other site is not
  counted — the name is whatever the sender writes. Empty takes every name, and
  the log says so at start.

**Both** of the protocols vote sites use are supported (NuVotifier v1 and v2), at
once — so it works on all of them rather than half.

**Which votes count (0.2.2).** `votes.count` picks the rule:

- `every-reset` — the default — counts every vote a site lets somebody make,
  held only to `votes.site-cooldown` (default `1h30m`) per site and person.
  Lists that let people vote every two hours pay every two hours. A site
  already refuses a vote before its own reset, so this never pays more than the
  site allows. Set `site-cooldown` a little under the sites' own reset (at least
  `1m`); `votes.site-cooldowns` gives a site its own interval, one
  `site=duration` each — `minecraft-mp.com=23h`.
- `once-a-day` counts one vote per site per voting day, the way Keystone did
  before 0.2.2 and most Votifier plugins do — for a server that wants a day's
  rewards to be the same whichever lists its players use.

Either way there is a daily ceiling, `votes.limit.per-day`. At 0 — the default —
it is worked out from the intervals: each site in `services` as many times as
its interval fits in a day (with no names listed, each line of `sites` at
`site-cooldown`, ten sites when there are none), so somebody who votes after
every reset never meets it, while a sender inventing votes does. The whole port
takes at most `votes.limit.per-minute` (30) a minute, counted per site, so one
noisy sender does not shut out the honest ones. 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.
`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. Streaks still count days.

**A vote that does not count says so in game:** too soon on that site (and when
it will count again) or the day's ceiling — at once if the player is online, on
their next join if not — beside the lines that already say a vote waits for time
played or a Discord link. One such line per player per five minutes, so votes
made up in somebody's name cannot fill their chat. A site's name is matched to
`services` however it is spelt, so capitals and spaces do not get a vote past
its interval. The editor's **Votes** screen shows the rules in force.

**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` (default 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 site's interval, one voting day's `limit.per-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.

**Only the owner changes vote rewards.** Rewards, streaks, party rewards and
crate loot run as the console, so in game only the owner (a level-4 operator) may
change them.

## The tab list

`tablist.toml` — what the list shows and how often it is redrawn (three seconds
by default). The header, the footer and the prefix and suffix on a name are
built out of ranks, so somebody who is a supporter and a moderator reads as both.

**Every rank's prefix (0.2.2).** `{prefix}` is the prefix of the heaviest group
somebody holds. `{prefixes}` is the prefix of every group they hold, heaviest
first, a space between — `[Staff] [VIP+]`. A group that another of theirs
inherits from is left out (VIP+ built on VIP shows only `[VIP+]`), and each
prefix starts from a clean style. It works in the tab list (`name`, header and
footer), in the chat format (`chat.format`) and in the join and leave lines
(`chat.presence`); `{prefix}` stays for servers that want only the heaviest.

**Ordering** is honoured where the game has a notion of one: from Minecraft
1.21.2 the client sorts by a number the server sends, and that number is the
rank's own weight. On older versions the only way is scoreboard teams, which
take over name colours, nameplate visibility and friendly fire from whatever
else is using them — so it is left alone there.

**What the tab list does today, the server does on its own**, and a vanilla
client sees it. Columns, faces and badges in the mod's own type need the client
half, and are not imitated here as "a list of names with brackets around them".
