Astral Realms Documentation Help

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

<dependency> <groupId>com.astralrealms</groupId> <artifactId>stats</artifactId> <version>1.0-SNAPSHOT</version> <scope>provided</scope> </dependency>

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:

import com.astralrealms.stats.AstralStats; AstralStats plugin = AstralStats.getPlugin(AstralStats.class);

Getter

Type

Use

plugin.stats()

StatService

Per-player StatContainers, statValue(...), addModifier/removeModifier. Backs StatsAPI.stat*.

plugin.resources()

ResourceService

Per-player ResourceContainers, resourceValue(...). Backs StatsAPI.resource(...).

plugin.combat()

CombatService

isInCombat(UUID) — per-player last-damage timestamps.

plugin.temporaryEffects()

TemporaryEffectService

Timed stat modifiers and timed resource effects. Backs StatsAPI.addTimedModifier/addTimedResourceEffect.

plugin.blueprints()

BlueprintService

The loaded Key → StatBlueprint map — see Stats & Modifiers.

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

double armor = StatsAPI.stat(player, StatType.ARMOR); // Keyed overload double armor = StatsAPI.stat(player, Key.key("astralstats", "armor")); // Key overload

Method

Behavior when the stat has no blueprint

stat(Player, Keyed)

Returns -1.0.

stat(Player, Key)

Returns -1.0.

stat(Player, Key, @Nullable Object context)

Returns -1.0. Context gates any ConditionalStatModifier on the stat — see Conditional and enum-based modifiers.

stat(Player, Keyed, double baseValue)

Returns baseValue unchanged.

stat(Player, Key, double baseValue)

Returns baseValue unchanged.

stat(Player, Keyed, double baseValue, @Nullable Object context)

Returns baseValue unchanged.

stat(Player, Key, double baseValue, @Nullable Object context)

Returns baseValue unchanged.

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

StatsAPI.addModifier(player, StatType.ATTACK_DAMAGE, modifier); // Keyed overload StatsAPI.addModifier(player, Key.key("astralstats", "attack_damage"), modifier); // Key overload StatsAPI.removeModifier(player, modifierKey);

Method

Behavior

addModifier(Player, Keyed statType, StatModifier)

Unwraps .key() and delegates to the Key overload.

addModifier(Player, Key statType, StatModifier)

Attaches the modifier to the player's StatInstance for statType (no-op if the player has no blueprint for that key) and marks it dirty.

removeModifier(Player, Key modifierKey)

Removes every modifier — on every stat the player has — whose own key() equals modifierKey. This is how a single source (an item unequipped, a buff ending) clears all the modifiers it granted with one call.

See StatModifier shape and Modifier computation for how FLAT/RELATIVE/PERCENTAGE combine.

Timed stat modifiers

StatsAPI.addTimedModifier(player, modifier, 2_500L); // online player StatsAPI.addTimedModifier(playerUuid, modifier, 2_500L); // possibly-offline player

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 PlayerQuitEvent the modifier is left in memory (just marked "unapplied") so it keeps counting down offline; the vanilla attribute/stat state resets naturally when the player's StatContainer unloads.

  • 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

// Drain 30 shield for 3 seconds, then restore it StatsAPI.addTimedResourceEffect(player, ResourceType.SHIELD, v -> v - 30, // apply v -> v + 30, // reverse 3_000L); StatsAPI.addTimedResourceEffect(playerUuid, ResourceType.SHIELD, apply, reverse, durationMs); // offline-safe overload

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, apply runs immediately against their live resource value.

  • If the player is offline, apply is deferred until they next join (if the effect hasn't expired by then).

  • On expiry, reverse runs against the player's current resource value (only if apply had actually run and the player is online at that moment).

  • On PlayerQuitEvent, any effect that was apply-ed is immediately reversed (reverse runs) and marked "unapplied" — this avoids the player silently losing/gaining resources while offline. On the next join, if the effect still hasn't expired, apply runs 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 apply nor reverse ever 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

double shield = StatsAPI.resource(player, ResourceType.SHIELD); // Drain 30 shield immediately (still clamped to [0, max-stat] by ResourceService) StatsAPI.resource(player, ResourceType.SHIELD, current -> current - 30);

Method

Behavior

resource(Player, ResourceType)

Returns the live amount, or 0.0 if the player has no container or no ResourceType configured for that type.

resource(Player, ResourceType, DoubleUnaryOperator)

Applies the operator to the current amount and re-clamps to [0, max-stat] (no-op if the resource isn't tracked for that player).

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

import com.astralrealms.stats.model.modifier.ModifierSource; import com.astralrealms.stats.model.modifier.ModifierType; import com.astralrealms.stats.model.modifier.impl.StatModifier; import net.kyori.adventure.key.Key; import org.bukkit.inventory.EquipmentSlot; StatModifier modifier = new StatModifier( Key.key("myplugin", "berserk_buff"), // key — identity used by removeModifier(...) StatType.ATTACK_DAMAGE.key(), // statKey — which stat this targets ModifierSource.OTHER, EquipmentSlot.SADDLE, // slot — descriptive metadata only, not read by compute() 15.0, // value ModifierType.FLAT ); StatsAPI.addModifier(player, StatType.ATTACK_DAMAGE, modifier);

Constructor argument

Type

Description

key

Key

The modifier's own identity. removeModifier(player, key) removes every modifier on the player whose key() matches this.

statKey

Key

Which stat (blueprinted StatType-style key) this modifier applies to.

source

ModifierSource

MAIN_HAND, OFF_HAND, ARMOR, OTHER, or VOID — informational provenance, not consumed by the compute path itself.

slot

EquipmentSlot

Equipment slot associated with the source. Stored as descriptive metadata only — nothing in StatInstance.compute or the shipped handlers reads it; ModifiersCommand itself always passes EquipmentSlot.SADDLE as a placeholder for command-created modifiers.

value

double

Magnitude, interpreted per type — see Modifier computation.

type

ModifierType

FLAT, RELATIVE, or PERCENTAGE.

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:

public class BiomeStatModifier extends ConditionalStatModifier<Biome> { private final Biome requiredBiome; public BiomeStatModifier(Key key, Key statKey, ModifierSource source, EquipmentSlot slot, double value, ModifierType type, Biome requiredBiome) { super(key, statKey, source, slot, value, type); this.requiredBiome = requiredBiome; } @Override protected boolean applies(Biome context) { return context == requiredBiome; } @Override protected Class<Biome> contextType() { return Biome.class; } }

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:

StatModifier modifier = new EnumBasedStatModifier<>( Key.key("myplugin", "desert_bonus"), StatType.MOVEMENT_SPEED.key(), ModifierSource.OTHER, EquipmentSlot.SADDLE, 10.0, ModifierType.PERCENTAGE, Biome.DESERT );

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.

import com.astralrealms.stats.registry.StatRegistry; import com.astralrealms.stats.model.stat.handler.StatHandler; import net.kyori.adventure.key.Key; StatRegistry.register(Key.key("myplugin", "mana_shield"), (player, instance, value) -> { // React to the computed value however this stat should manifest for the player. player.setGlowing(value > 0); });

Member

Signature

Notes

StatRegistry.register(Keyed, StatHandler)

void

Unwraps .key() and delegates to the Key overload.

StatRegistry.register(Key, StatHandler)

void

Throws IllegalArgumentException if a handler is already registered for that key — each key can only be bound once per server run. StatContainer.register(...) resolves the handler at blueprint-registration time (player join, or /stats reload), so bind it from your plugin's onEnable() — before any player next joins or reloads.

StatRegistry.unregister(Key)

void

Removes the binding. Existing StatInstances already built with the old handler are unaffected until the player's container is rebuilt (e.g. /stats reload, rejoin).

StatRegistry.findHandlerByKey(Key)

Optional<StatHandler>

Lookup used internally by StatContainer.register(...) when a blueprint loads.

StatHandler contract

public interface StatHandler { void apply(Player player, StatInstance instance, double value); }

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

AttributeStatHandler(Attribute attribute, double playerBaseValue)

Mirrors value onto a vanilla Attribute via a single astralstats:main AttributeModifier (ADD_NUMBER, amount value - playerBaseValue), replacing any modifier it previously applied. See AttributeStatHandler behavior.

DummyStatHandler

No-op apply(...). The registration default for any key with no explicit handler — the value is still tracked and readable through StatsAPI.stat(...), but nothing is pushed onto the player automatically.

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 by StatsAPI directly.

See also

  • Stats & ModifiersStatType catalog, blueprint format, modifier computation math.

  • ResourcesResourceType, resources.yml, regeneration formulas.

  • Combat Mechanics — how combat()/CombatService and shield absorption fit together.

  • Placeholders — read-only access via %stats_...% for anything that doesn't need the Java API.

Last modified: 25 July 2026