CommonerVote turns guests into Commoners. Team members vote for guests whose building they have seen; each vote is
worth a share of a promotion, and when a guest reaches 100 % the plugin promotes them through LuckPerms, welcomes them
and announces it to the network. Votes last a limited time, and a staff vote can count for more than a team vote.
Everything players see is a Paper dialog, the action bar or chat. This page describes 2.0.0, the latest release
on master (tag v2.0.0), a rewrite for Paper 26.2 and Java 25. The source is public at
MCME/CommonerVote.
At a glance
| Repository | MCME/CommonerVote |
| Release branch | master (work goes to development first) |
| Version described | 2.0.0 (tag v2.0.0, 2026-10-21) |
| Platform | Paper, api-version: '26.2'; one plugin per backend server |
| Build | Maven, JDK 25 (release 25), nothing shaded |
| Depends on | LuckPerms (hard, depend); a MySQL or MariaDB database (required, the driver comes with Paper); a Velocity proxy with the BungeeCord channel enabled (optional, for cross-server updates) |
How the code is organised
Everything lives under com.mcmiddleearth.commonervote. The services talk to small interfaces, so the tests can swap
the outside world (database, LuckPerms, dialogs, proxy) for fakes.
| Package | Job |
|---|---|
| (root) | CommonerVotePlugin builds every service once on enable and stops the database thread on disable. Permissions holds the node names, spelled as in plugin.yml. |
command |
VoteCommand builds the Brigadier tree for /vote. A subcommand the sender may not use is cut from the tree through its requires, so the client never sees it. |
domain |
Plain values and rules: Settings (the shared settings, their defaults and ranges), VoteMath (scores and rescaling), PromotionGuard (who may be promoted automatically), Vote, IntRange, and the form objects behind the settings dialogs. |
service |
The flows: VoteService (cast, renew, withdraw, progress), ReviewService (candidates, clear), PromotionService (automatic and manual promotions, the welcome), SettingsService and SettingsCache (shared settings), XpBarService (the progress shown on the XP bar), plus helpers for names, messages and console commands. |
storage |
JDBC over java.sql only. DbExecutor runs every piece of database work on one dedicated thread, with a fresh connection each time. VoteStore, SettingsStore and PromotionStore own one table each; Schema creates the tables. |
rank |
RankService (interface) and LuckPermsRankService: name lookups, last known names, the exempt check for offline players, and a player's direct groups. Every answer is a future completed on LuckPerms' threads. |
network |
Messenger (interface), BungeeMessenger and BungeeCodec: the proxy plugin messages (see How it talks to other plugins). |
ui |
DialogPresenter (interface) and PaperDialogPresenter; ui/spec describes each dialog, ui/view holds the nine views (welcome, guest info, candidate list, vote, confirm, review list and detail, settings, texts). |
listener |
PlayerListener: join (promotion check, pending welcome), quit, and XP changes (the XP bar is a progress meter for guests only). |
Two things to know before you change code:
- Threads. Database work never runs on the main thread, and LuckPerms answers arrive on LuckPerms' threads. Any
code that touches the Bukkit API after either hops back to the main thread first (the services get a main-thread
Executorfor that). - Settings refresh. The settings live in the database and are shared by every server on it. Each server reads them
again every minute (1,200 ticks); until the first read succeeds, nobody is promoted and/vote configdoes not open.
If the database is down at start-up, the table set-up is retried on the same timer.
Building and testing
You need JDK 25 and Maven.
mvn -B verify
This compiles, runs the tests and writes the jar to target/. The tests use JUnit, Mockito and MockBukkit
(mockbukkit-v26.2), with H2 in MariaDB mode standing in for the database. TestCommonerVotePlugin overrides the
protected create… methods of the main class to plug in the in-memory database, inline executors and fakes.
LegacyCompatibilityTest keeps the votes table readable by the old 1.2.9 plugin, and PluginDescriptorTest checks
plugin.yml.
The jar name follows MCME's convention, CommonerVote-<version>-<DEV|SNAPSHOT|RELEASE>-<commit id>.jar:
- the kind defaults to
DEV; pass-Dbuild.type=RELEASEfor a release build; - the commit id is the current HEAD's, so build after committing;
- in a linked git worktree the commit-id plugin reads the wrong commit, so pass
-Dgit.commit.id.abbrev=$(git rev-parse --short=7 HEAD)yourself.
GitHub Actions (.github/workflows/build.yml) runs mvn -B verify on pushes to master, development and
revision/**, and on pull requests. It does not publish anything. Releases are on the repository's
Releases page, as the RELEASE jar plus its .sha256.
Configuration
plugins/CommonerVote/config.yml holds only the database connection and the proxy switch. Everything else is a
shared setting in the database.
| Key | Shipped value | Meaning |
|---|---|---|
database.ip |
localhost |
Database host. Keep it on localhost or a private network. |
database.port |
3306 |
Database port. |
database.dbName |
development |
Database name. It must exist; the plugin creates its tables. |
database.user |
development |
Database user. Needs SELECT, INSERT, UPDATE, DELETE and CREATE. |
database.password |
development |
Database password (a placeholder: replace it). |
useBungee |
true |
Send cross-server updates and announcements through the proxy. Set false on a single server. |
The keys of 1.2.9 that moved into the database (validityPeriod, votesNeeded, useXpBar and others) are ignored;
the start-up log names any your file still has.
Shared settings (database)
Edited in game with /vote config and stored in the commonervote_settings table. Another server picks up a change
within about a minute.
| Setting | Default | Range / notes |
|---|---|---|
votesNeeded |
5 |
1–20. Changing it rescales every stored vote. |
validityDays |
100 |
1–365. Older votes stop counting but stay in the table. |
staffWeight |
2 |
1–5 votes per staff vote. |
teamWeight |
1 |
1–5 votes per other vote. |
autoPromote |
true |
Promote at 100 % without an admin. |
xpBar |
true |
Show guests' progress on the XP bar. |
broadcastNetwork |
true |
Announce promotions to the whole network. |
commonerGroup |
commoner |
The LuckPerms group a promotion gives (<group>). |
promotableGroups |
empty | Groups besides default that do not block an automatic promotion. |
promotionCommand |
lp user <player> parent set <group> |
The command a promotion runs; <player> becomes the UUID. Must contain <player>. |
rulesUrl |
https://www.mcmiddleearth.com/help/terms |
Button in the welcome dialog. |
guideUrl |
empty | "Building guide" button; hidden when empty. |
guestInfoText, welcomeText, announceText |
see the README | MiniMessage, with <player>, <votes_needed>, <validity_days>. |
Database tables
The plugin creates these with CREATE TABLE IF NOT EXISTS:
| Table | Holds |
|---|---|
commonervote_votes |
One row per vote: voter and recipient UUIDs, time (epoch ms), weight (a share of a promotion) and reason. Unchanged from 1.2.9, so the old plugin can still read it. |
commonervote_settings |
The shared settings, one name/value row each. |
commonervote_promotions |
Who was promoted, when, how (auto, manual, detected) and whether they have seen the welcome. |
Commands and permissions
/vote is registered with Brigadier (there is no commands: block in plugin.yml); /commonervote:vote works too.
| Command | What it does | Permission |
|---|---|---|
/vote |
Team: the guests online on this server. Others: their own progress, or "already a Commoner". | none (what you see depends on commonervote.vote) |
/vote <player> |
The vote dialog for a player (cast, renew, withdraw, optional reason). | commonervote.vote |
/vote withdraw <player> |
Withdraw your vote. | commonervote.vote |
/vote score |
Your own progress. | none |
/vote score <player> |
A player's progress. | commonervote.score.other or commonervote.review |
/vote review [player] |
The candidates, or the votes for one player. | commonervote.review |
/vote clear <player> |
Delete every vote for a player, after a confirmation. | commonervote.clear |
/vote promote <player> |
Promote now, after a confirmation. | commonervote.promote |
/vote config |
The settings dialogs (players only). | commonervote.config |
/vote help |
The subcommands you may use. | none |
| Permission | Default | Grants |
| --- | --- | --- |
commonervote.vote |
op | Cast, renew and withdraw votes; the guest list. |
commonervote.staff |
op | Votes count with staffWeight. Includes commonervote.vote. |
commonervote.exempt |
op | Marks a player as already a Commoner or higher: cannot be voted for, never promoted, normal XP. Give it to the Commoner group and every rank above. |
commonervote.score |
true | Kept for compatibility; no command checks it. |
commonervote.score.other |
op | /vote score <player>. |
commonervote.review |
op | Review dialogs and /vote review. |
commonervote.clear |
op | Clear a player's votes. |
commonervote.config |
op | The settings dialogs. |
commonervote.promote |
op | "Promote now" and /vote promote. |
commonervote.* |
op | Everything above except commonervote.exempt. |
Dialog buttons check the permission again when clicked, so removing a permission takes effect at once.
APIs and extension points
CommonerVote offers no Java API and fires no events for other plugins, and it is not published to the
Maven repository. What other software can rely on:
- The database tables above. The votes table layout is a compatibility promise towards 1.2.9; keep it unchanged.
- The promotion command. Promotions run a configurable console command, so a network can promote through
something other thanlp user … parent setif it needs to.
Inside the plugin, RankService, Messenger, DialogPresenter and CommandRunner are the seams: a new rank plugin or
transport means a new implementation of one interface, not changes to the services.
How it talks to other plugins and systems
| System | Direction | How |
|---|---|---|
| LuckPerms | reads, and writes through a command | API: find players by name (any case, also offline), last known names, commonervote.exempt for offline players, direct groups. The promotion itself is the console command promotionCommand, with the player's UUID. |
| Velocity proxy | both | BungeeCord plugin messages on the BungeeCord channel (needs bungee-plugin-message-channel = true in velocity.toml). |
| MySQL / MariaDB | reads and writes | JDBC through the driver Paper ships; one database shared by every server. |
| Paper | uses | Dialog API for every screen; Paper's profile cache for names of offline players. |
Proxy messages. A vote for a guest on another server goes out as ForwardToPlayer to that guest's name, subchannel
CommonerVote, with the payload Voted (new in 2.0) or Update (votes changed: refresh the XP bar and check for a
promotion; 1.2.9 understands this one too). The promotion announcement is MessageRaw to ALL, carrying the message as
JSON. Plugin messages need a player online on the sending server; when nobody is, the guest is brought up to date at
their next join.
The promotion guard. lp user <player> parent set <group> replaces all of a player's groups. So the plugin promotes
only a player whose direct groups are default plus the groups in promotableGroups; anyone else is listed as
"Auto-promotion blocked" in the review, and "Promote now" names the group that blocks it.
flowchart LR
T[Team member: /vote] --> V[VoteService]
V --> DB[(commonervote_* tables)]
V --> P[PromotionService]
P -->|direct groups, exempt| LP[LuckPerms API]
P -->|console: promotionCommand| LP
V -->|ForwardToPlayer, subchannel CommonerVote| PX[Velocity proxy]
P -->|MessageRaw ALL| PX
PX --> O[Guest's server: XP bar, action bar, promotion check]
Further reading
- README and
CHANGELOG onmaster, including the upgrade notes
from 1.x - Setting up for JDK 25, Maven and MockBukkit