Astral Realms Documentation Help

Boosters

A booster is a timed, consumable item that multiplies the experience and/or money a player earns from job actions. Boosters are defined as blueprints on disk, handed out as items, activated by right-clicking, and tracked server-side as timed BoosterEntity rows that feed the same ModifierService used by permission-based multipliers and the Developer API.

Booster blueprints

Blueprints live under plugins/AstralJobs/boosters/ (BoosterBlueprintService#load, called on enable and on /jobs reload). Every .yml file in the folder is loaded as one BoosterBlueprint, the same one-file-per-object convention used by jobs/*.yml — the fields below sit at the file's root, not nested under a wrapping key. A blueprint whose id duplicates an already-loaded one is logged as a warning and skipped; a file that fails to parse is logged and skipped without aborting the rest of the load.

Minimal example

id: "individual-experience" type: "experience" scope: "individual" duration: "30m" multiplier: 1.15 item: material: "DIAMOND" name: "Personal Experience Booster" lore: - "Gain extra insights from your own experiences." - "Boosts learning efficiency by 15%."

Top-level fields

Field

Type

Description

id

String

Blueprint identifier. Referenced by /give, the stacksuppliers placeholder, and stored on the item's PDC so it can be identified back to its blueprint on right-click.

type

experience \|money \|both

Which earnings the booster multiplies — deserialises to BoosterType. See type.

scope

individual \|global

Who the active booster applies to — deserialises to BoosterScope. See Scope and conflicts.

duration

Duration string

How long the booster stays active once activated, parsed by AstralCore's DurationParser (e.g. 30m, 1h, 1d 5h).

multiplier

double

The earnings multiplier. See multiplier for the exact conversion applied.

item

ItemStackWrapper

The physical item players activate. See item.

type

BoosterType has three values:

YAML value

Enum

Effect

experience

EXPERIENCE

Multiplies job experience gains only.

money

MONEY

Multiplies job money gains only.

both

BOTH

Multiplies both experience and money gains.

type also selects the message shown for %booster_formattedType% (JobsMessages#TYPE_EXPERIENCE/TYPE_MONEY/TYPE_GLOBAL) and, via JobModifierScope.fromBoosterType, which modifier scope the booster applies to.

scope

BoosterScope is INDIVIDUAL or GLOBAL:

  • individual — only the player who activates the item is boosted.

  • global — every online player is boosted for the booster's duration, and the booster keeps applying to players who join while it's active.

See Scope and conflicts for the activation rules each scope enforces.

multiplier

multiplier is converted to a percentage bonus before it's applied as a RELATIVE JobModifier (BoosterService#apply):

  • If multiplier > 1.0 (factor form), the bonus is (multiplier - 1.0) * 100. 1.15+15%.

  • Otherwise (bonus form), the bonus is multiplier * 100. 0.15+15%.

Both forms of a given percentage are accepted and produce the same result; the shipped blueprints use the factor form (1.15, 1.20, …).

item

item is a standard AstralCore ItemStackWrapper — the shipped blueprints only set material, name, and lore, but the wrapper also accepts copy-from, enchantments, item-flags, amount, and components; see the Menu Items reference for the full schema. The item is built fresh on every request (BoosterBlueprintService#build) through RootPlaceholderContainer — i.e. name/lore placeholders resolve against global context, not the specific player receiving the item — and the resulting stack has the blueprint id stamped into its PDC under astraljobs:booster_id, which is how BoostersListener and BoosterBlueprintService#fromItemStack recognise a held/right-clicked item as a booster.

Scope and conflicts

Right-clicking a booster item (BoostersListener, cancels the interaction) hands it to BoosterService#use, which:

  1. Removes the item from the player's inventory (refunded via the mailbox if anything below fails).

  2. Checks whether the player already owns an active booster of the same type and scope (BoosterRepository#hasActiveBooster). If so, the item is refunded and the player gets JobsMessages#SAME_TYPE_BOOSTER_ACTIVE — activation is blocked.

  3. Otherwise creates a BoosterEntity:

    • individual — activates immediately (activatesAt = now) for that player only.

    • global — looks up the most recently queued active global booster of the same type (BoosterRepository#findLastGloballyQueued, regardless of who owns it) and, if one exists, chains the new booster's activatesAt to that booster's expiresAt instead of starting immediately — global boosters of the same type queue back-to-back rather than stacking concurrently. If none is queued, it starts immediately.

  4. Saves the entity, sends PERSONAL_BOOSTER_ACTIVATED or GLOBAL_BOOSTER_ACTIVATED to the activator, and — if it's immediately active — applies it (to every online player for global) and fires BoosterActivateEvent.

Because the same-type check is scoped to (player, type, scope), an individual booster and a global booster of the same type can be active on a player at the same time — their RELATIVE modifiers stack multiplicatively (see Modifier integration). Only two boosters that share both type and scope conflict.

Activation lifecycle

A BoosterWatchdogTask runs every second (Bukkit.getScheduler().runTaskTimerAsynchronously, period 20L) over every cached BoosterEntity (BoosterService#boosters):

  • Expired (entity.isExpired()) — BoosterService#expire un-applies the modifier from the owner (or every online player, for global), deletes the entity, fires BoosterExpiredEvent, and sends PERSONAL_BOOSTER_EXPIRED/GLOBAL_BOOSTER_EXPIRED to the owner if online.

  • Active but not yet announced (entity.isActive()) — BoosterService#activate applies the modifier (idempotent — keyed by the entity's own UUID-derived modifierKey(), so a re-apply is harmless) and, the first time a given entity crosses into active, fires BoosterActivateEvent. This mainly covers queued global boosters reaching their activatesAt, and boosters resumed after a restart; immediately-active boosters are already announced by use() and marked so the watchdog doesn't double-announce them.

Individual boosters pause while their owner is offline (BoosterService#unload stamps updatedAt) and shift their expiresAt forward by the offline duration on the next join (BoosterService#loadresume()) — so logging off doesn't burn down a personal booster's timer. Global boosters run in wall-clock time regardless of who's online, and are re-cached for late joiners so they still see (and receive) any booster active when they connect.

Both BoosterActivateEvent and BoosterExpiredEvent extend BoosterEvent, exposing the BoosterEntity (getBooster()); see the Developer API for listening to them from another plugin.

Obtaining booster items

Booster items are exposed through AstralCore's item-stack-supplier registry under the jobs namespace (JobsBoostersItemSupplier, registered as CommonRegistries.itemStackSuppliers().register("jobs", …)):

Surface

Form

Resolves via

/give (AstralCore)

/give <player> jobs:<blueprint-id> [amount]

Key.key(itemId) matched against the supplier's completions() (every loaded blueprint id).

stacksuppliers placeholder

%stacksuppliers_jobs_<blueprint-id>%

JobsBoostersItemSupplier#get(String)BoosterBlueprintService#build.

/give Steve jobs:individual-experience
actions: - "[give-item] %stacksuppliers_jobs_global-money%"

JobsBoostersItemSupplier#keyOf(ItemStack) is the reverse lookup (used by generic supplier-aware tooling) — it reads the PDC tag back into Key.key("jobs", blueprintId) via BoosterBlueprintService#fromItemStack.

Permission-based boosters

Separate from timed booster items, config.yml's permissions-boosters map is an always-on multiplier layer applied on join — PlayerConnectionListener#onPlayerJoin calls BoosterService#applyPermissionModifiers once per player:

permissions-boosters: "example.permission.booster1": EXPERIENCE: 2.0 MONEY: 1.5 "example.permission.booster2": EXPERIENCE: 1.5 MONEY: 2.0

Each entry maps a permission node to a BoosterType → multiplier table (EXPERIENCE/MONEY/BOTH, same factor-or-bonus conversion as multiplier). JobsConfiguration#permissionBoostersFor walks the map in declaration order and keeps the last matching permission's table — a player holding several of these permissions does not get them stacked, only the last match in config order applies. Each resulting multiplier is applied as a RELATIVE JobModifier keyed astraljobs:permission_booster/<scope>, so it persists for the session rather than expiring like a booster item.

Modifier integration

Both booster items and permission boosters ultimately register a RELATIVE JobModifier against ModifierService, scoped by JobModifierScope.fromBoosterType:

BoosterType

JobModifierScope

EXPERIENCE

EXPERIENCE

MONEY

MONEY

BOTH

GLOBAL

When PlayerJobModifiers#compute runs for an EXPERIENCE or MONEY gain, it applies every modifier whose scope matches the requested scope or is GLOBAL — so a both-type booster (or a both-type permission entry) boosts both experience and money income at once, alongside any type-specific boosters/permissions already active. RELATIVE modifiers combine multiplicatively (multiplyScalar *= 1 + value/100); a booster item's modifier is removed the moment it expires or is un-applied, while a permission modifier stays for as long as the player holds the permission and is online.

Where to go next

  • Overview — how boosters fit alongside the rest of the earning pipeline.

  • Configurationconfig.yml's permissions-boosters and other top-level keys.

  • Placeholders — the %booster_*% namespace exposed by both BoosterBlueprint and BoosterEntity.

  • Messages — the full booster message-key catalogue.

  • Developer APIBoosterActivateEvent, BoosterExpiredEvent, and JobsAPI modifier access.

Last modified: 25 July 2026