AstralRanks exposes read/write access to rank progression through the static RanksAPI façade and the RankService/MenuService services hung off the main plugin instance. It registers three actions and two requirements for use inside its own configuration, and persists per-player progress through AstralSync rather than firing any custom Bukkit events.
A @UtilityClass of static entry points, initialized with the plugin instance on onEnable (RanksAPI.init(this)). Safe to call from any other plugin once AstralRanks has finished loading.
import com.astralrealms.ranks.RanksAPI;
Optional<RankConfiguration> next = RanksAPI.nextRank(player);
boolean done = RanksAPI.hasCompletedRequirement(player, "roturier", "eco", "eco");
boolean doneOnNext = RanksAPI.hasCompletedRequirement(player, "eco", "eco"); // uses the player's next rank
boolean category = RanksAPI.hasCompletedCategory(player, "roturier", "eco");
boolean categoryOnNext = RanksAPI.hasCompletedCategory(player, "eco"); // uses the player's next rank
List<String> ranks = RanksAPI.rankIds();
List<String> categories = RanksAPI.categoriesIds();
Method
Returns
Behavior
nextRank(Player)
Optional<RankConfiguration>
Delegates to RankService#getNextRank. Empty when the player already holds the last rank's permission (next-rank absent) — or when the chain is broken past their current rank (next-rank points at an id that failed to load). Otherwise falls back to the default-group rank if the player holds none of the chain's permissions at all.
Delegates to RankService#hasCompletedCategory — true only if every requirement under categoryId on that rank has its progress >= its amount.
hasCompletedCategory(Player, String categoryId)
boolean
Overload without a rank id — resolves nextRank(player) first; false if the player has no next rank.
rankIds()
List<String>
Every loaded rank's id, from RankService#all() (unordered — not the progression chain order; see getSortedRanks).
categoriesIds()
List<String>
Every loaded category's id, from AstralRanks#categoriesConfiguration() (categories.yml).
RankService
Accessed via AstralRanks#ranks(). Owns rank loading, the progression chain, and every unlock/requirement mutation — see Ranks & Progression for the full narrative behind these methods.
Every loaded rank, unmodifiable copy, keyed by id.
getSortedRanks()
List<RankConfiguration>
The progression chain, default-group first, built once at load time by following next-rank.
getNextRank(Player)
RankConfiguration
The rank right after the player's current rank in the chain; null if the player already holds the last rank's permission (next-rank absent) or if next-rank points at an id that isn't loaded; falls back to the default-group rank if the player holds none of the chain's permissions at all. Not an Optional — always null-check (contrast with RanksAPI.nextRank, which wraps this in one).
isNextRank(Player, String rankId)
boolean
true only when rankId equals getNextRank(player)'s id and the player holds at least one chain permission — unlike getNextRank, this does not fall back to default-group.
true when the player has a loaded PlayerRankData snapshot and every requirement on the rank has progress >= its amount. Does not also check isNextRank — a rank further down the chain can report canUnlock == true while still being unreachable.
unlock(Player, RankConfiguration)
void
Chain-gated: sends CANNOT_UNLOCK and returns immediately if isNextRank(player, rank.id()) is false. The requirement check that follows is not actually gating — for the first unmet requirement it sends CANNOT_UNLOCK, but the loop's break only exits the requirement loop; execution falls through regardless and still runs actions(), sends UNLOCK_SUCCESS, and broadcasts UNLOCK_BROADCAST (unless the rank id is in ignore-broadcast). Callers driving unlocks programmatically should gate on canUnlock themselves rather than relying on unlock to refuse. See Unlocking a rank.
Calls the requirement's take(player, current), applies the returned amount according to shouldCountUntilFull(), and pays out the category's rewards once every requirement in it is complete.
false if the rank, the matching requirement, or the snapshot isn't found; otherwise compares stored progress against amount.
Registered actions
AstralRanks registers three actions with registerAction (its own local action registry — notregisterActionGlobally, the mechanism AstralCore uses for network-wide built-ins like console and give-item; see Actions). In practice this means they resolve inside any actions:/deny-actions: list parsed by AstralRanks itself — rank actions:, category rewards:, and AstralRanks' own menus/*.yml — the same scope the shipped menus already use them in.
unlock-rank
Looks up a rank by id and calls RankService#unlock for the executing player; sends RANK_NOT_FOUND if the id doesn't resolve. Full behavior documented under Unlocking a rank/unlock-rank action.
Arg
Type
Description
(positional)
PlaceholderWrapper<String>
Rank id to attempt to unlock.
actions:
- "[unlock-rank] %parameters_rank_id%"
update-rank-requirement
Looks up a rank by id and calls RankService#updateRequirement with the given category and requirement ids; sends RANK_NOT_FOUND if the rank id doesn't resolve.
Arg
Type
Description
rankId
PlaceholderWrapper<String>
Rank the requirement belongs to.
category
PlaceholderWrapper<String>
Category the requirement is grouped under.
requirement
PlaceholderWrapper<String>
Requirement id (the key under the rank's requirements map).
Toggles which category's missions are shown (e.g. on a scoreboard) by writing to PlayerRankData's categoryQuestsDisplay field. Re-passing the currently-displayed category clears it (QUESTS_DISPLAY_DISABLED); any other value sets it (QUESTS_DISPLAY_ENABLED). Sends UNEXPECTED_ERROR if the player's PlayerRankData snapshot isn't available.
Arg
Type
Description
categoryWrapper
PlaceholderWrapper<String>
Category id to display, or a caller-defined sentinel (the shipped menus use the literal string all to mean "every category") — the action does not validate this against categoriesIds().
AstralRanks registers two requirements with registerRequirement (same local-registry scoping as the actions above — see Requirements for the general [id] args/ object-form syntax).
has-completed-category
Wraps PlayerRankData#hasCompletedCategory directly — note this checks the cached "already rewarded" flag set by RankService#updateRequirement once a category's rewards have been paid out, not a live recomputation of requirement progress (that's what RanksAPI.hasCompletedCategory/RankService #hasCompletedCategory do instead).
A fresh PlayerRankData(new HashMap<>(), new HashMap<>(), null) for a player with no prior snapshot.
deserialize(DataHolder, BinaryMessage)
Reads progress (Map<String rankId, Map<String requirementId, Integer>>), completedCategories (Map<String rankId, Set<String categoryId>>), then an optional trailing string for categoryQuestsDisplay — read only if (binaryMessage.remaining() > 0), so snapshots written before that field existed still deserialize cleanly with it left null.
Writes the same three fields back, categoryQuestsDisplay as an optional string.
update(Player, PlayerRankData)
No-op — returns the data unchanged.
apply(Player, PlayerRankData)
No-op — rank progress has no in-world effect to reapply on load (unlocking already runs its own actions(), e.g. LuckPerms group swaps, at unlock time).
PlayerRankData itself:
Field / Method
Type
Description
progress
Map<String, Map<String, Integer>>
rank id → (requirement id → stored progress amount).
completedCategories
Map<String, Set<String>>
rank id → set of category ids whose rewards have already been paid out.
categoryQuestsDisplay
String?
The category id currently selected for display (e.g. on a scoreboard) by set-displayed-ranks-quests, or null.
setRankProgress(String rank, String key, int value)
Because it round-trips through AstralSync's binary snapshot format, rank progress follows the player across every server in the network. Mutating progress/completedCategories directly bypasses the reward payout and success/broadcast messages in RankService#updateRequirement/unlock — prefer the update-rank-requirement/unlock-rank actions for normal flows, and only touch the snapshot directly for admin tooling.
Adding a new requirement type
RankRequirement is a small contract:
public interface RankRequirement {
int amount();
boolean shouldCountUntilFull();
CompletableFuture<Integer> take(Player player, int currentValue);
}
Method
Meaning
amount()
The target value progress must reach.
shouldCountUntilFull()
true: take's return value is a delta — the amount actually consumed/gained by this call — and gets added to existing progress (experience, economy, item all return true; each call takes only up to the remaining gap). false: take's return value is a snapshot of the player's current total (job returns false; JobsLevelRankRequirement returns JobsAPI.getTotalLevels(player) uncapped) — progress is only ever written, in one jump to the full amount(), once that snapshot meets or exceeds it; sub-threshold snapshots are not persisted, so %parameter_requirement_current% reads 0 until the requirement is actually satisfied.
take(Player, int currentValue)
Does the actual work (withdraw currency/items/XP, or just read a live total) and returns the value RankService#updateRequirement uses per the rule above. currentValue is the requirement's current stored progress, so implementations can compute amount() - currentValue as "how much is still needed".
The four built-in types (experience, job, economy, item) live in com.astralrealms.ranks.model.requirement — ExperienceRankRequirement, JobsLevelRankRequirement, CurrencyRankRequirement, ItemRankRequirement. Adding a fifth means:
Implement a new RankRequirement (typically a record, e.g. record MyRankRequirement(int amount, ...)).
Add a matching case "my-type" -> { ...; yield new MyRankRequirement(...); } branch to the switch in RankRequirementTypeSerializer#deserialize, reading whatever extra config fields the type needs off the ConfigurationNode (see the economy case reading both currency and amount, versus experience reading only amount).
Rebuild and redeploy the plugin.
MenuService
Accessed via AstralRanks#menus(). Thin wrapper around a core MenuContainer for opening AstralRanks' three menus programmatically.
rank → the given rank; category → the given category.
No custom Bukkit events
AstralRanks fires no custom Bukkit events. Other plugins observe rank state through RanksAPI/RankService for live checks, or by reading PlayerRankData directly for a snapshot — there is no RankUnlockEvent or similar to hook into. If you need to react to an unlock, poll canUnlock/hasCompletedCategory (e.g. from your own requirement or a scheduled check), or hook the LuckPerms/economy/etc. side effects that a rank's own actions: already produce.
Where to go next
AstralRanks Overview — the high-level progression/mission model and player-data shape.
Ranks & Progression — the permission chain, unlock flow, and RankConfiguration YAML.
Requirements & Missions — requirement types, categories, and the full update-rank-requirement progress/reward flow.
Placeholders — the ranks_*/rank_*/category_*/requirement_* namespaces these services back.
Configuration — config.yml, categories.yml, and rank/menu file locations.
Commands — /ranks and /mission subcommands and permissions.