Astral Realms Documentation Help

Developer API

AstralJobs exposes read access to job levels/experience and modifier management through the static JobsAPI façade, reacts to XP/money/level/reward changes through a set of cancellable Bukkit events, and registers one action and one item-stack supplier that other plugins/menus can drive directly.

Maven coordinates

<dependency> <groupId>com.astralrealms</groupId> <artifactId>jobs</artifactId> <version>2.0-SNAPSHOT</version> <scope>provided</scope> </dependency>

Repository: https://maven.astralrealms.fr/repository/maven-public/.

JobsAPI

A @UtilityClass of static entry points, initialized with the plugin instance on onEnable. Safe to call from any other plugin once AstralJobs has finished loading.

import com.astralrealms.jobs.JobsAPI; int level = JobsAPI.getJobLevel(player, "miner"); // -1 if the player has no level for that job double xp = JobsAPI.getJobExperience(player, "miner"); // -1.0 if the player has no level for that job int totalLevels = JobsAPI.getTotalLevels(player); // sum of level across every job JobsAPI.addModifier(player.getUniqueId(), modifier); // see JobModifier below JobsAPI.hasModifier(player.getUniqueId(), modifierKey); JobsAPI.removeModifier(player.getUniqueId(), modifierKey);

Method

Returns

Behavior

getJobLevel(Player\|UUID, String jobId)

int

The player's current level for jobId, or -1 if no JobLevelEntity is cached for that player/job.

getJobExperience(Player\|UUID, String jobId)

double

The player's current experience for jobId, or -1.0 under the same condition.

getTotalLevels(Player\|UUID)

int

Sum of level across every JobLevelEntity cached for that player (one per job once loaded — see below).

addModifier(UUID, JobModifier)

void

Delegates to ModifierService#addModifier.

removeModifier(UUID, Key)

void

Delegates to ModifierService#removeModifier.

hasModifier(UUID, Key)

boolean

Delegates to ModifierService#hasModifier.

Modifier API

JobsAPI.addModifier/removeModifier/hasModifier are thin wrappers around ModifierService, which keeps one Map<Key, JobModifier> per player (PlayerJobModifiers), backing both timed boosters and the permissions-boosters config feature.

JobModifier

public record JobModifier(Key key, double value, JobModifierType type, JobModifierScope scope, @Nullable String jobId)

Field

Type

Description

key

Key

Unique identifier for this modifier. Re-adding a modifier with the same key overwrites the previous one (PlayerJobModifiers is keyed by Key) — reapplying is safe/idempotent.

value

double

The modifier magnitude. Meaning depends on type (see below).

type

JobModifierType

RELATIVE — percentage, combined multiplicatively across all matching modifiers (multiplyScalar *= 1 + value/100). ABSOLUTE — flat amount, summed across all matching modifiers, applied before the relative scalar (modifiedValue += value).

scope

JobModifierScope

EXPERIENCE, MONEY, or GLOBAL. A GLOBAL-scoped modifier applies to both experience and money computations; EXPERIENCE/MONEY modifiers apply only to a request for that same scope.

jobId

String?

Optional. When set, the modifier only applies to gains for that specific job (isJobSpecific() returns true). When null, it applies across every job.

A convenience constructor omits jobId (defaults to null, i.e. applies to all jobs):

new JobModifier(key, value, type, scope); // all jobs new JobModifier(key, value, type, scope, "miner"); // "miner" only

ModifierService#compute(playerId, scope, baseValue, jobId) is what actually folds a player's modifiers into a gain: it skips modifiers whose scope doesn't match the request (unless GLOBAL) and skips job-specific modifiers whose jobId doesn't match the requested job, then applies every surviving ABSOLUTE modifier's value as a sum and every surviving RELATIVE modifier's value as a multiplicative percentage, in that order ((baseValue + Σabsolute) × Π(1 + relative/100)). This is the exact math behind both the action-resolution pipeline and the jobs-main menu's live "computed" value.

import net.kyori.adventure.key.Key; import com.astralrealms.jobs.model.modifier.JobModifier; import com.astralrealms.jobs.model.modifier.JobModifierType; import com.astralrealms.jobs.model.modifier.JobModifierScope; // +25% experience on every job, until explicitly removed or the player logs out Key key = Key.key("myplugin", "vip_xp_boost"); JobsAPI.addModifier(player.getUniqueId(), new JobModifier(key, 25.0, JobModifierType.RELATIVE, JobModifierScope.EXPERIENCE)); boolean active = JobsAPI.hasModifier(player.getUniqueId(), key); JobsAPI.removeModifier(player.getUniqueId(), key);

Job events

All job events live under com.astralrealms.jobs.event.job and extend the abstract JobEvent (getPlayer(), getJob()). ActionService fires JobExperienceGainEvent, JobMoneyGainEvent, and JobLevelUpEvent from its 2-thread worker pool (constructed with async = true) — handlers must not touch non-thread-safe Bukkit API without scheduling back onto the main thread. JobClaimRewardEvent is the exception: it is not async (async = false), since it is fired synchronously from the /jobs claim command handler.

Event

Cancellable

Async

Fired from

JobExperienceGainEvent

Yes

Yes

ActionService per matched tracked action.

JobMoneyGainEvent

Yes

Yes

ActionService per matched tracked action.

JobLevelUpEvent

Yes

Yes

ActionService, once per level gained inside the level-up loop.

JobClaimRewardEvent

No

No

JobsCommand#onClaim (/jobs claim), once per reward tier claimed.

JobExperienceGainEvent

public JobExperienceGainEvent(Player player, Job job, double originalExperience, double gain)

Accessor

Type

Description

getOriginalExperience()

double

The player's experience for this job before this gain (immutable).

getGain()

double

The raw XP amount computed for this action, after equations and modifiers but before this event runs (immutable).

getNewExperience()/setNewExperience(double)

double

Initialized to originalExperience + gain. Mutate this, not gain, to change the amount actually applied.

The XP actually credited is max(0, newExperience - originalExperience) after the event returns — cancelling the event zeroes it outright. Rewriting newExperience to something other than originalExperience + gain (e.g. doubling the gain, or clamping it) changes only that one action's payout; it does not touch the player's already-stored experience.

JobMoneyGainEvent

public JobMoneyGainEvent(Player player, Job job, double amount)

Accessor

Type

Description

getAmount()

double

The raw money amount computed for this action (immutable).

getNewAmount()/setNewAmount(double)

double

Initialized to amount. This is the amount actually granted — unlike the XP event, there is no "original" to diff against.

The money actually granted is max(0, newAmount) after the event returns; cancelling zeroes it. Note the asymmetry with JobExperienceGainEvent: here newAmount is the payout, not a delta over a stored base. This event fires for every matched action independently of whether that action also triggers a level-up.

JobLevelUpEvent

public JobLevelUpEvent(Player player, Job job, int level)

Accessor

Type

Description

getLevel()

int

The level the player is about to reach.

Fired once per level inside ActionService's level-up loop, before the level/experience are actually mutated. Cancelling it stops the loop immediately (break) — the player keeps the experience accumulated so far but does not gain this level or any subsequent one in the same action. See Level-up flow.

JobClaimRewardEvent

public JobClaimRewardEvent(Player player, Job job)

No additional accessors beyond getPlayer()/getJob() — notably no level. /jobs claim fires one instance of this event per reward tier it marks claimed, so a player claiming three unclaimed tiers on miner in one command fires it three times, each carrying the same Job but no way to distinguish which tier. Not cancellable and not async — safe to touch other Bukkit API directly from a listener.

Booster events

Booster events live under com.astralrealms.jobs.event.booster and extend BoosterEvent (getBooster()BoosterEntity). Unlike the job events above, BoosterEvent's isAsync flag is not fixed — it's computed at construction time from the calling thread (super(!Bukkit.isPrimaryThread())). In practice both events fire from BoosterService's repository-callback continuations (after a save/delete completes), which normally run off the main thread, but a listener should not hard-code that assumption — check event.isAsync() if it matters.

Event

Fired from

Notes

BoosterActivateEvent

BoosterService#use (immediate activation) and BoosterService#activate (watchdog-driven activation, e.g. a queued global booster reaching its activatesAt).

Fired once per booster activation, guarded so a booster doesn't double-announce.

BoosterExpiredEvent

BoosterService#expire, after the booster row has been deleted from the database.

Fired regardless of whether the booster's owner is currently online.

Neither event is Cancellable — by the time either fires, the modifier has already been applied (for activation) or the entity has already been deleted from the database (for expiry). Use them for reactive side effects (logging, notifying other players, integrations), not to veto the booster lifecycle.

BoosterActivateEvent

public BoosterActivateEvent(BoosterEntity booster)

BoosterExpiredEvent

public BoosterExpiredEvent(BoosterEntity booster)

BoosterEntity (see Boosters) exposes getUniqueId(), getOwnerId(), getType() (EXPERIENCE/MONEY/BOTH), getScope() (INDIVIDUAL/GLOBAL), getDuration(), getMultiplier(), isActive()/isExpired(), and modifierKey() — the Key (astraljobs:<uniqueId>) under which the booster's JobModifier is registered in ModifierService.

claim-job-reward action

Registered globally (registerAction("claim-job-reward", ClaimJobRewardAction.class)) so it's usable from any menu/dialog/reward actions: list on the network, not just AstralJobs' own menus. Like every action, both arguments accept placeholder substitution — a shipped-style rewards menu row would resolve level from its iteration context rather than hardcoding it.

- "[claim-job-reward] miner 10" - "[claim-job-reward] miner %parameters_level%"

Arg

Type

Description

jobId

Job placeholder

Required. Resolves to a loaded Job by id.

level

Integer placeholder

Required. The reward tier to claim.

Behavior: looks up the executing player's JobsPlayerData via SyncAPI.findData; if the player's cached level for jobId is below level, sends REWARD_CANNOT_BE_CLAIMED and stops. Otherwise looks up the JobReward for that exact level — sending UNEXPECTED_ERROR if none exists — runs the reward's actions against the player, then marks (jobId, level) claimed in JobsPlayerData. Also sends UNEXPECTED_ERROR if the player's JobsPlayerData snapshot isn't available (e.g. Sync hasn't finished loading it yet). Unlike /jobs claim, this does not check whether the tier was already claimed before re-running its actions — the action is meant for a menu that already hides claimed tiers (e.g. via the claimed/claimable reward placeholders).

Booster item-stack supplier

Booster items are exposed to AstralCore's item-stack-supplier registry under the namespace jobs:

CommonRegistries.itemStackSuppliers().register("jobs", new JobsBoostersItemSupplier(this));

That means any AstralCore call site that accepts a supplier-backed item key also accepts a jobs:<id> key, where <id> is a BoosterBlueprint's id (the filename under boosters/, minus extension). The supplier:

  • get(String id) — builds a fresh ItemStack from the blueprint via BoosterBlueprintService#build; throws IllegalArgumentException if no blueprint has that id.

  • get(Player, Key, int amount) — same lookup, then overrides the stack size.

  • completions() — every loaded blueprint id, as Key.key("jobs", id), for tab completion.

  • keyOf(ItemStack) — reverses an ItemStack back to Key.key("jobs", blueprint.id()) via BoosterBlueprintService#fromItemStack, or null if the stack isn't a recognized booster item.

# anywhere an item-stack-supplier key is accepted actions: - "[give-item] jobs:double_xp_15m"

AstralSync integration

JobsSnapshotAdapter registers JobsPlayerData (claimed rewards) with AstralSync under the key astraljobs:data:

SyncAPI.registerAdapter(new JobsSnapshotAdapter());

Method

Behavior

key()

astraljobs:data.

create(Player)

A fresh JobsPlayerData(new HashMap<>()) for a player with no prior snapshot.

deserialize(DataHolder, BinaryMessage)

Reads a Map<String jobId, Set<Integer> levels> off the wire.

serialize(DataHolder, JobsPlayerData, BinaryMessage)

Writes that same map back.

update(Player, JobsPlayerData)

No-op — returns the data unchanged.

apply(Player, JobsPlayerData)

No-op — claimed-rewards state has no in-world effect to reapply on load.

JobsPlayerData itself is a Map<String, Set<Integer>> of job id → claimed levels, with claimReward(jobId, level) and hasClaimedReward(jobId, level) helpers. Other plugins can read it the same way AstralJobs does internally:

import com.astralrealms.jobs.storage.JobsPlayerData; import com.astralrealms.sync.SyncAPI; SyncAPI.findData(player.getUniqueId(), JobsPlayerData.class) .ifPresent(data -> { boolean claimed = data.hasClaimedReward("miner", 10); });

Because it round-trips through AstralSync's binary snapshot format rather than a local table, claimed-reward state — unlike modifiers — follows the player across every server in the network. See Rewards for how /jobs claim and the claim-job-reward action populate it.

Last modified: 25 July 2026