Ranks & Progression
A rank is one YAML file describing an id, a permission node, a next-rank pointer to the following rank in the chain, a display item, a set of requirements ("missions"), and the actions to run when a player unlocks it. RankService loads every rank file, links them into an ordered chain by following next-rank, and infers a player's current rank purely from which group.* permission they hold — nothing about "current rank" is stored by the plugin itself. See AstralRanks Overview for the high-level model and Requirements & Missions for requirement types and category rewards in depth.
Loading ranks
RankService#load resolves plugins/AstralRanks/ranks/:
If the folder doesn't exist yet, it is created and a single file,
ranks/default.yml, is copied out of the jar as a seed. No other rank file is auto-extracted.The folder is walked recursively (
Files.walk) for every regular file ending in.ymlor.yaml— subfolders are scanned too, so ranks can be organised into sub-directories.Each file is deserialised to a
RankConfiguration. A file that fails to parse is logged and skipped; the rest continue loading.If a rank's
idis already registered (from an earlier file in the walk), the duplicate is logged as a warning (Duplicate rank ID found: <id> in file: <path>) and skipped — the first-loaded file for that id wins. File walk order is filesystem-dependent, so which file "wins" is not guaranteed when duplicates exist.After loading, the plugin looks up
config.yml'sdefault-groupid among the loaded ranks. If it's missing, startup fails hard withIllegalStateException("Default rank not found: <id>")— unlike a single bad file, a missing default rank is fatal.The chain is rebuilt (
sortedRanks, see below).
/ranks reload re-runs the whole loadConfiguration() pipeline, including this load — see Commands.
Top-level fields
Field | Type | Description |
|---|---|---|
| String | Rank identifier. Used as the map key, in |
| ComponentWrapper | MiniMessage name for the rank, exposed as |
| String | Permission node that marks a player as having reached this rank, e.g. |
| String | Id of the following rank in the chain. Omit (or leave absent) on the last rank — |
| ItemStackWrapper | Item shown for this rank in menus — see Menu Items. Exposed as |
|
| Missions gating this rank's unlock, keyed by requirement id. Each entry also carries a |
| PaperActionList | Actions run once, on the main thread, when the rank is unlocked — see Actions. |
The progression chain
RankService#sortedRanks starts at the rank whose id is config.yml's default-group and follows each rank's next-rank pointer, building an ordered List<RankConfiguration>:
If a rank's
next-rankpoints at an id that isn't loaded, the walk logs a warning (Rank '<id>' points to non-existent next rank '<next>'. Stopping sort.) and stops — every rank after that point is unreachable through the chain (even if its file loaded fine).If following
next-rankpointers ever revisits a rank id already in the chain (a cycle), the walk stops silently, with no warning — the chain simply ends at the last non-repeated entry.
A player's current rank is derived, not stored: RankService#isNextRank/getNextRank walk sortedRanks from the front, and for each entry in order check player.hasPermission(rank.permission()). The walk stops at the first rank the player does not hold permission for; the last rank before that stop is their current rank, and the entry right after it is their "next rank" — the one whose requirements are currently active:
getNextRank(player)returns that next entry, ornullif the current rank is the last one in the chain (next-rankabsent). If the player holds none of the chain's permissions at all, it falls back to thedefault-grouprank.isNextRank(player, rankId)returnstrueonly whenrankIdequals that next entry's id. UnlikegetNextRank, it does not fall back todefault-group— if the player holds none of the chain's permissions, it returnsfalsefor every rank id.
This means a player's permissions are expected to form a contiguous prefix of the chain starting at default-group; a permission grant out of order (holding a later rank's permission without the ones before it) is read as if the walk stopped at the first gap.
Unlocking a rank
RankService#unlock(player, configuration) is the single unlock entry point, called by the unlock-rank action:
Chain check — if
isNextRank(player, configuration.id())isfalse, the player is sentCANNOT_UNLOCKand nothing else happens. This is the only check inunlock()that actually stops the unlock.Requirement check — the player's
PlayerRankData(viaSyncAPI) is looked up; if found, the code loops overconfiguration.requirements()and sendsCANNOT_UNLOCKon the first requirement whose stored progress is below itsRankRequirement#amount(), thenbreaks out of the loop. This does not stop the unlock — see the warning below.Actions —
configuration.actions().run(player)is scheduled withBukkit.getScheduler().runTask, so it always executes on the main thread, unconditionally on step 2's outcome. A failure (ExecutableRunException) is logged and stops the unlock before any success message is sent.Success message —
RanksMessages.UNLOCK_SUCCESSis sent to the player, with the rank registered as a placeholder (%rank_*%resolves againstconfiguration).Broadcast — unless
configuration.id()is listed inconfig.yml'signore-broadcast,RanksMessages.UNLOCK_BROADCASTis broadcast network-wide through the coreChatService.
Note that steps 2–5 all run inside the same SyncAPI.findData(...).ifPresent(...) callback — if the player has no PlayerRankData loaded, unlock silently does nothing past the initial isNextRank check (no message is sent for that case).
unlock-rank action
Registered by AstralRanks as unlock-rank (UnlockRankAction). Looks up a rank by id and calls RankService#unlock for the executing player; if the id doesn't resolve, sends RANK_NOT_FOUND instead.
Argument | Type | Description |
|---|---|---|
(positional) | PlaceholderWrapper\<String\> | Rank id to attempt to unlock. |
This is how the shipped rank-details menu (menus/categories.yml) drives the unlock button: its rang_unlock item is gated by view-requirements: ["[compare] %parameters_rank_unlockable% == true"] and fires [unlock-rank] %parameters_rank_id% followed by [close] on click; a rang_lock item (lower priority, same slots, no view-requirements) is the fallback shown while requirements aren't yet met.
The ranks-list menu
menus/ranks.yml defines menu id ranks-list (opened by /ranks, via MenuService#openMainMenu). Its ranks layout iterates provider: "%parameters_ranks%" — the sorted chain, passed in by MenuService#openMainMenu as the ranks open-parameter — tainted rank, and resolves three competing item variants per slot via view-requirements and priority (higher priority wins among items that pass their requirements for the same slot):
Item | Priority |
| Meaning |
|---|---|---|---|
| 3 |
| Player already holds this rank's permission — already unlocked. |
| 2 |
| This is the player's current next rank in the chain. Click opens |
| 1 |
| Fallback — any rank the player doesn't hold permission for and that isn't the current next rank (a future, not-yet-reachable rank). |
%parameter_rank_available% is backed by RankConfiguration's available placeholder key, which calls RankService#isNextRank — so the rank item only ever renders for the single rank that is actually next in the player's chain; every other locked rank falls through to rank-lock. Note that this menu only shows chain eligibility (available), not mission completion (unlockable) — a player can open the rank item before their missions are done; the mission gate lives in the rank-details menu's rang_unlock/rang_lock items (see unlock-rank action).
Rank as a placeholder
RankConfiguration implements ComplexPlaceholder under namespace rank — any placeholder container that registers a rank (menu item context, %parameters_rank% open-parameters, unlock/broadcast messages) exposes:
Placeholder | Type | Description |
|---|---|---|
| String | The rank's |
| ItemStack | Rendered |
| Component |
|
| String |
|
| String | The rank's |
| ItemProvider | The rank's |
| Boolean | Requires player context. |
| Boolean | Requires player context. |
Resolving available or unlockable outside a player context (e.g. from console) throws IllegalArgumentException. See Placeholders for the full namespace catalogue, including requirement_* (a WrappedRankRequirement).
default.yml walkthrough
The seeded starter rank (ranks/default.yml) shows the full shape:
displayis the item shown for this rank throughout theranks-list/rank-detailsmenus (%rank_item%).requirementsis grouped one-entry-per-category in this file (eco/eco,lumberjack/lumberjack, …) but the category id is independent of the requirement id — a rank can put several requirements under one sharedcategoryso they complete, and reward, together. See Requirements & Missions for the requirementtypes (economy,job,experience,item) and how category completion pays out rewards.actionsis a plain LuckPerms group swap —parent add vip/parent remove default— run through the coreconsoleaction whenunlock()reaches step 3 (see the requirement-check caveat above). This is the mechanism by which unlocking a rank actually changes a player's permissions (and, by extension, which entryisNextRank/getNextRanktreat as "current" from then on).
Where to go next
Requirements & Missions — requirement
types, categories, and reward payout.Placeholders — full
ranks_*/rank_*/category_*/requirement_*catalogue.Developer API —
unlock-rank,update-rank-requirement, and the registered requirement checks.Commands —
/rankssubcommands, includingreload.