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
BoostersListener has two entry points, both @EventHandler(priority = HIGH, ignoreCancelled = true), and both cancel their event and hand the item to BoosterService#use:
PlayerInteractEvent— right-clicks only; a non-RIGHT_CLICK_AIRaction on an already-cancelled event is ignored, and aRIGHT_CLICK_BLOCKon a block whose state is anInventoryHolderis ignored too (so right-clicking a chest while holding a booster doesn't consume it).PlayerItemConsumeEvent— covers blueprints whoseitemuses an edible/drinkable material (e.g. a potion), which would otherwise start the vanilla consume animation. Neither path lets vanilla consumption happen.
Either way BoosterBlueprintService#fromItemStack must recognise the stack as a booster; BoosterService#use then:
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 and notifies the activator on a three-way branch:
INDIVIDUAL→personal-booster-activated;GLOBALand immediately active →global-booster-activated;GLOBALbut queued behind another →global-booster-queued.If it's immediately active, applies it (to every online player for
global), firesBoosterActivateEvent, and — for aGLOBALbooster reaching its first activation — broadcastsglobal-booster-broadcastserver-wide.
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 additively (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, broadcastsglobal-booster-broadcastif it is aglobalbooster, then sendsglobal-booster-activatedto the owner (if online) andglobal-booster-activated-notificationto every other online player. The messages cover queuedglobalboosters reaching theiractivatesAt; immediately-active boosters are already announced byuse()and marked so the watchdog doesn't double-announce them, and aglobalbooster that is already running when the server starts is pre-marked the same way byBoosterService's constructor, so a restart never re-announces it. A personal booster re-armed after a restart still firesBoosterActivateEventhere (it sends no message).
The server-wide global-booster-broadcast therefore fires exactly once per booster, whichever path activates it first: use() and activate() share one atomic first-activation set keyed by the booster's UUID, and only the caller that wins the add() broadcasts.
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 additively: their percentages are summed and the combined factor is applied once, so the exact formula is (baseValue + Σabsolute) × (1 + Σ(relative / 100)) — two +50% modifiers give ×2, not ×2.25. 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.
Boosters are not the only way in. The add-job-modifier and remove-job-modifier actions register and drop a JobModifier directly from any menu, dialog or reward actions: list — including ABSOLUTE modifiers and modifiers restricted to a single job, neither of which a booster item can express. Like every entry in ModifierService, they are in-memory only and do not survive an unload.
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.