Astral Realms Documentation Help

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/:

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

  2. The folder is walked recursively (Files.walk) for every regular file ending in .yml or .yaml — subfolders are scanned too, so ranks can be organised into sub-directories.

  3. Each file is deserialised to a RankConfiguration. A file that fails to parse is logged and skipped; the rest continue loading.

  4. If a rank's id is 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.

  5. After loading, the plugin looks up config.yml's default-group id among the loaded ranks. If it's missing, startup fails hard with IllegalStateException("Default rank not found: <id>") — unlike a single bad file, a missing default rank is fatal.

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

id

String

Rank identifier. Used as the map key, in next-rank pointers, and as the rank id passed to unlock-rank/update-rank-requirement.

display-name

ComponentWrapper

MiniMessage name for the rank, exposed as %rank_name%.

permission

String

Permission node that marks a player as having reached this rank, e.g. group.apprenti. Checked with Player#hasPermission.

next-rank

String

Id of the following rank in the chain. Omit (or leave absent) on the last rank — RankConfiguration#nextRank is null at the end of the chain.

display

ItemStackWrapper

Item shown for this rank in menus — see Menu Items. Exposed as %rank_item%.

requirements

Map<String, WrappedRankRequirement>

Missions gating this rank's unlock, keyed by requirement id. Each entry also carries a category id (grouped and rewarded separately — see Requirements & Missions).

actions

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-rank points 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-rank pointers 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, or null if the current rank is the last one in the chain (next-rank absent). If the player holds none of the chain's permissions at all, it falls back to the default-group rank.

  • isNextRank(player, rankId) returns true only when rankId equals that next entry's id. Unlike getNextRank, it does not fall back to default-group — if the player holds none of the chain's permissions, it returns false for 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:

  1. Chain check — if isNextRank(player, configuration.id()) is false, the player is sent CANNOT_UNLOCK and nothing else happens. This is the only check in unlock() that actually stops the unlock.

  2. Requirement check — the player's PlayerRankData (via SyncAPI) is looked up; if found, the code loops over configuration.requirements() and sends CANNOT_UNLOCK on the first requirement whose stored progress is below its RankRequirement#amount(), then breaks out of the loop. This does not stop the unlock — see the warning below.

  3. Actionsconfiguration.actions().run(player) is scheduled with Bukkit.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.

  4. Success messageRanksMessages.UNLOCK_SUCCESS is sent to the player, with the rank registered as a placeholder (%rank_*% resolves against configuration).

  5. Broadcast — unless configuration.id() is listed in config.yml's ignore-broadcast, RanksMessages.UNLOCK_BROADCAST is broadcast network-wide through the core ChatService.

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.

actions: - "[unlock-rank] %parameters_rank_id%"

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

view-requirements

Meaning

rank-complete

3

[permission] %parameter_rank_permission%

Player already holds this rank's permission — already unlocked.

rank

2

[compare] %parameter_rank_available% == true, [permission] !%parameter_rank_permission%

This is the player's current next rank in the chain. Click opens rank-details:rank=%parameter_rank%.

rank-lock

1

[permission] !%parameter_rank_permission%

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

%rank_id%

String

The rank's id.

%rank_item%

ItemStack

Rendered display, resolved through context.function().

%rank_name%

Component

display-name.

%rank_next-rank%

String

next-rank id, or null on the last rank.

%rank_permission%

String

The rank's permission node.

%rank_requirements%

ItemProvider

The rank's requirements values — use as a layout provider.

%rank_available%

Boolean

Requires player context. RankService#isNextRank(player, id) — is this rank the player's current next rank.

%rank_unlockable%

Boolean

Requires player context. RankService#canUnlock(player, this) — are all of this rank's requirements met (independent of chain position).

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:

id: "default" display-name: "<#999999><bold>default" permission: "group.default" next-rank: "roturier" display: material: "paper" name: "<bold><primary_color>Vilain" lore: - "..." item-flags: - HIDE_ATTRIBUTES requirements: eco: category: "eco" display-item: material: GOLD_INGOT name: "<#267ad9><bold>Mission Richesse" lore: - " <#e3dbcc>Progression : <#26d971>%parameter_requirement_current%/%parameter_requirement_total%" requirement: type: economy currency: "default" amount: 250 lumberjack: category: "lumberjack" display-item: material: "oak_log" name: "<#267ad9><bold>Mission Bûcheron" requirement: type: item material: OAK_LOG amount: 8 # ... jobs-level, player-level, miner, farmer, fisherman, hunter, other actions: - "[console] lp user %player_name% parent add vip" - "[console] lp user %player_name% parent remove default"
  • display is the item shown for this rank throughout the ranks-list/rank-details menus (%rank_item%).

  • requirements is 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 shared category so they complete, and reward, together. See Requirements & Missions for the requirement types (economy, job, experience, item) and how category completion pays out rewards.

  • actions is a plain LuckPerms group swap — parent add vip/parent remove default — run through the core console action when unlock() 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 entry isNextRank/getNextRank treat 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 APIunlock-rank, update-rank-requirement, and the registered requirement checks.

  • Commands/ranks subcommands, including reload.

Last modified: 25 July 2026