Developer API
AstralStats exposes its whole programmatic surface through the static StatsAPI façade, the static StatRegistry, and a handful of services hung off the main plugin instance — there is no custom Bukkit event to hook. Other plugins use StatsAPI to read/modify a player's stats and resources, and StatRegistry to bind a StatHandler that makes a stat actually do something to the player.
Maven coordinates
Repository: https://maven.astralrealms.fr/repository/maven-public/.
Accessing services
StatsAPI covers the common read/write paths; anything beyond that goes through the plugin instance directly:
Getter | Type | Use |
|---|---|---|
|
| Per-player |
|
| Per-player |
|
|
|
|
| Timed stat modifiers and timed resource effects. Backs |
|
| The loaded |
StatsAPI is initialized from AstralStats.onEnable() (StatsAPI.initialize(this)) before commands and placeholders are registered, so it is safe to call from any other plugin's onEnable() as long as AstralStats is a hard/soft dependency that loads first.
StatsAPI
com.astralrealms.stats.StatsAPI is a Lombok @UtilityClass — every member below is static.
Reading a stat value
Method | Behavior when the stat has no blueprint |
|---|---|
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
| Returns |
Every overload resolves through StatService.statValue(...); the Keyed overloads just unwrap .key() and delegate to the matching Key overload. See Value semantics for integrators for why the -1.0/baseValue split matters, and Value lookup for the underlying table.
The baseValue overloads compute the modifier chain against the value you supply rather than the stat's own stored base — useful for rolling a one-off number (e.g. an item's own numeric stat) through a player's modifiers without touching their persistent stat state.
Adding and removing modifiers
Method | Behavior |
|---|---|
| Unwraps |
| Attaches the modifier to the player's |
| Removes every modifier — on every stat the player has — whose own |
See StatModifier shape and Modifier computation for how FLAT/RELATIVE/PERCENTAGE combine.
Timed stat modifiers
Both overloads delegate to TemporaryEffectService.addTimedStatModifier. Semantics:
If the player is online, the modifier is applied immediately (same effect as
addModifier).If the player is offline, the modifier is queued and applied the next time they join — if they reconnect before it expires.
A tick task (every Bukkit tick) removes expired modifiers automatically, calling
StatsAPI.removeModifier-equivalent cleanup for online players.On
PlayerQuitEventthe modifier is left in memory (just marked "unapplied") so it keeps counting down offline; the vanilla attribute/stat state resets naturally when the player'sStatContainerunloads.On reconnect (
PlayerJoinEvent), any modifier that hasn't yet expired is re-applied and its countdown continues; any modifier that expired while the player was offline is discarded silently — it is never applied or reversed.If the server restarts, all pending timed modifiers are lost (in-memory only) — this is the deliberate safe default: no stuck modifiers surviving a crash.
Timed resource effects
Both overloads delegate to TemporaryEffectService.addTimedResourceEffect(playerId, resourceType, apply, reverse, durationMs). Unlike timed stat modifiers, a resource effect is reversed on disconnect, not left pending:
If the player is online,
applyruns immediately against their live resource value.If the player is offline,
applyis deferred until they next join (if the effect hasn't expired by then).On expiry,
reverseruns against the player's current resource value (only ifapplyhad actually run and the player is online at that moment).On
PlayerQuitEvent, any effect that wasapply-ed is immediately reversed (reverseruns) and marked "unapplied" — this avoids the player silently losing/gaining resources while offline. On the next join, if the effect still hasn't expired,applyruns again and the countdown continues from where the tick loop left it (the duration is not extended by the disconnect).If the player never reconnects before expiry, neither
applynorreverseever runs a second time — there's nothing left to undo.
apply/reverse are plain DoubleUnaryOperators over the resource's current amount — write reverse as the true inverse of apply or the resource will drift. See StatsAPI.resource(...) for the non-timed resource entry points and Resources for how a resource's value is clamped to its max-stat.
Resource access
Method | Behavior |
|---|---|
| Returns the live amount, or |
| Applies the operator to the current amount and re-clamps to |
ResourceType.HEALTH is special-cased at the command layer (/stats resource) but not inside StatsAPI.resource(...) itself — calling it with HEALTH only does anything if HEALTH is configured as a tracked resource in resources.yml, which it isn't by default. See Resource types.
Constructing a StatModifier
Constructor argument | Type | Description |
|---|---|---|
|
| The modifier's own identity. |
|
| Which stat (blueprinted |
|
|
|
|
| Equipment slot associated with the source. Stored as descriptive metadata only — nothing in |
|
| Magnitude, interpreted per |
|
|
|
A second constructor accepts an explicit UUID uniqueId as the first argument (InstanceModifier's identity, distinct from key) for callers that need to track/remove one specific modifier instance rather than every modifier under a shared key; when omitted, a random UUID is generated.
Conditional and enum-based modifiers
ConditionalStatModifier<T> (abstract, extends StatModifier) only contributes to StatInstance.compute(base, context) when a caller-supplied context object matches. Extend it directly for a custom condition:
EnumBasedStatModifier<T extends Enum<?>> is the shipped concrete implementation and covers the common case — it applies only when the supplied context equals one fixed enum constant:
The context object is only ever supplied by the caller of stat(..., context)/StatService.statValue(..., context) — pass whatever enum constant makes sense (damage cause, equipment type, biome category, etc.); non-ConditionalStatModifier instances always apply unconditionally regardless of context.
Registering custom stat behavior
By itself a stat is just a computed double — nothing pushes it onto the player unless a StatHandler is bound to its key in StatRegistry. AstralStats binds 26 of its 34 shipped StatType keys to AttributeStatHandlers at class-init (see Attribute-backed stats); anything else — including a brand-new custom key — falls back to DummyStatHandler (see Unregistered stats) unless another plugin registers one.
Member | Signature | Notes |
|---|---|---|
|
| Unwraps |
|
| Throws |
|
| Removes the binding. Existing |
|
| Lookup used internally by |
StatHandler contract
apply is called once per StatInstance whenever that instance is dirtied (a modifier added/removed, or the base value changed) and the once-per-second StatsUpdateTask tick runs — see Update loop. value is the already-computed result of instance.compute(instance.baseValue()); the handler's job is purely to do something with it.
Shipped implementation | Behavior |
|---|---|
| Mirrors |
| No-op |
A custom StatHandler is the correct extension point when a stat should drive something AstralStats itself doesn't know about (a custom mana bar, an external permission gate, a visual effect) rather than a vanilla attribute — bind it with StatRegistry.register and ship a matching blueprint so the key actually becomes active for players (see Adding a custom stat).
Value semantics for integrators
statValue (and therefore every no-baseValue StatsAPI.stat(...) overload) returns -1.0 when the player has no StatInstance for the requested key — i.e. no blueprint is loaded for that key, so the StatContainer never registered it. This is a deliberate sentinel, not a real stat value: always treat a negative result from the no-baseValue overloads as "this stat doesn't exist for this player" rather than folding it into arithmetic.
The baseValue overloads sidestep this entirely: when the player has no blueprint for the key, they return the caller-supplied baseValue unchanged instead of -1.0; when the player does have the stat, they still compute against the supplied baseValue rather than the stat's own stored base. Prefer the baseValue overloads whenever the caller already has a sensible fallback/base number in hand (e.g. an item's own rolled stat) — they never produce a bogus negative result.
No custom events
AstralStats does not fire any custom Bukkit events. Its listeners (PlayerListener, CriticalDamageListener, DamageReductionListener, ShieldListener, CombatListener) only consume vanilla PlayerJoinEvent/PlayerQuitEvent/EntityDamageEvent/EntityDamageByEntityEvent — they don't publish anything of their own. The entire integration surface for other plugins is:
StatsAPI— read/modify stats and resources, permanent or timed.StatRegistry— bind custom behavior to a stat key.The services exposed off
AstralStats(stats(),resources(),combat(),temporaryEffects(),blueprints()) for anything not covered byStatsAPIdirectly.
See also
Stats & Modifiers —
StatTypecatalog, blueprint format, modifier computation math.Resources —
ResourceType,resources.yml, regeneration formulas.Combat Mechanics — how
combat()/CombatServiceand shield absorption fit together.Placeholders — read-only access via
%stats_...%for anything that doesn't need the Java API.