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
Top-level fields
Field | Type | Description |
|---|---|---|
| String | Blueprint identifier. Referenced by |
|
| Which earnings the booster multiplies — deserialises to |
|
| Who the active booster applies to — deserialises to |
| Duration string | How long the booster stays active once activated, parsed by AstralCore's |
| double | The earnings multiplier. See |
|
| The physical item players activate. See |
type
BoosterType has three values:
YAML value | Enum | Effect |
|---|---|---|
|
| Multiplies job experience gains only. |
|
| Multiplies job money gains only. |
|
| 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:
Removes the item from the player's inventory (refunded via the mailbox if anything below fails).
Checks whether the player already owns an active booster of the same
typeandscope(BoosterRepository#hasActiveBooster). If so, the item is refunded and the player getsJobsMessages#SAME_TYPE_BOOSTER_ACTIVE— activation is blocked.Otherwise creates a
BoosterEntity:individual— activates immediately (activatesAt= now) for that player only.global— looks up the most recently queued activeglobalbooster of the sametype(BoosterRepository#findLastGloballyQueued, regardless of who owns it) and, if one exists, chains the new booster'sactivatesAtto that booster'sexpiresAtinstead of starting immediately — global boosters of the same type queue back-to-back rather than stacking concurrently. If none is queued, it starts immediately.
Saves the entity, sends
PERSONAL_BOOSTER_ACTIVATEDorGLOBAL_BOOSTER_ACTIVATEDto the activator, and — if it's immediately active — applies it (to every online player forglobal) and firesBoosterActivateEvent.
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#expireun-applies the modifier from the owner (or every online player, forglobal), deletes the entity, firesBoosterExpiredEvent, and sendsPERSONAL_BOOSTER_EXPIRED/GLOBAL_BOOSTER_EXPIREDto the owner if online.Active but not yet announced (
entity.isActive()) —BoosterService#activateapplies the modifier (idempotent — keyed by the entity's own UUID-derivedmodifierKey(), so a re-apply is harmless) and, the first time a given entity crosses into active, firesBoosterActivateEvent. This mainly covers queuedglobalboosters reaching theiractivatesAt, and boosters resumed after a restart; immediately-active boosters are already announced byuse()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#load → resume()) — 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 |
|---|---|---|
|
|
|
|
|
|
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:
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:
|
|
|---|---|
|
|
|
|
|
|
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.
Configuration —
config.yml'spermissions-boostersand other top-level keys.Placeholders — the
%booster_*%namespace exposed by bothBoosterBlueprintandBoosterEntity.Messages — the full booster message-key catalogue.
Developer API —
BoosterActivateEvent,BoosterExpiredEvent, andJobsAPImodifier access.