Developer API
AstralPets exposes its behaviour through eight actions registered into AstralCore's global action registry, two ItemStackSupplier namespaces, a SyncAPI snapshot adapter for cross-server pet data, a set of custom Bukkit events, and a compile-time effect registry. There is no static PetsAPI façade — external plugins reach everything through the AstralPets plugin instance and SyncAPI.
Maven coordinates
Repository: https://maven.astralrealms.fr/repository/maven-public/.
Plugin handle and services
AstralPets is annotated @Getter (Lombok, fluent — no get prefix), so every field below is a same-named accessor:
Getter | Type | Use |
|---|---|---|
|
| Look up pet blueprints by id, list all loaded ones, build item stacks. |
|
| Look up food blueprints by id, build/identify food item stacks. |
|
| Build a pet |
|
| Spawn/despawn/find the |
|
| Look up skins by id or by pet, build/identify skin items, apply a skin, resolve the AstralMobs spawn overrides for a pet. |
|
| Open the main pet menu, a specific pet's menu, or the pokedex menu. |
|
| AstralCore's generic dialog container, loading |
|
| The plugin-scoped transformers from |
|
| The loaded |
|
| The loaded |
|
| The |
|
| The |
|
| The |
|
|
|
BlueprintService/FoodBlueprintService
PetService
gainExperience fires PetGainExperienceEvent first and reads back event.experienceGained(), so a listener can shrink or zero out the actual amount applied. It then loops level-ups, firing PetLevelUpEvent once per level and reading event.newLevel() back — a listener can change how far the pet actually levels on this call.
EntityService
spawn despawns any existing entity for the player first, then fires PetSpawnEvent — a null return means the event was cancelled, no collision-free spot was found within 5 blocks up/down of the target location, or the pet's AstralMobs mob-blueprint does not exist. The world entity itself is spawned by AstralMobs (MobsAPI.spawnOwnedMob), so PetEntity#entityId, uniqueId and bukkitLocation only carry real values after that call returns. ride is a no-op unless the pet's blueprint has a rideable effect at level ≥ 1 and the entity has no passenger already; if the level requirement isn't met it sends PET_CANNOT_BE_RODE with the level still required.
SkinService
apply enforces both rules itself — a pet that already wears a skin, and a skin the pet's blueprint does not list, are refused with a message and the item is left alone — then fires PetSkinApplyEvent. It hops to the main thread when called from a packet thread, returning false and doing the work a tick later; buildItemStack throws for a skin that declares no item. See Pet Skins.
MenuService
openPetMenu passes Map.of("pet", pet) into MenuContainer#computeAndOpen, which lands in the menu's parameters map (Menu#findParameter) — inside that menu, %parameters_pet% resolves to the raw Pet object itself. That's the concrete source of the Pet that rename-pet resolves its first argument against.
Registered actions
AstralPets registers 11 actions into AstralCore's action registry (AstralPets#onEnable), usable in any menu, dialog, or actions: list. Arguments are positional, space-separated, and placeholder-resolved at runtime — same convention as AstralCore's own actions.
ID | Argument(s) | Description |
|---|---|---|
| Marks an owned pet active. | |
| Marks an active pet inactive. | |
| Spawns an owned pet at the player's location. | |
| Removes the pet's spawned entity, if any. | |
| Converts an owned pet back into a mailed item and removes it from | |
| Opens the pet's storage inventory (no-op if it has no storage). | |
| Mounts the player on the pet's currently-spawned entity. | |
| Renames a pet, resolving the |
pet-id in every action except rename-pet is a PlaceholderWrapper<UUID> — a placeholder chain that resolves to the pet's UUID, typically %pet_id% when the action runs from a context whose placeholder subject is already a Pet (e.g. an item in a menu built from %pets_all%/%pets_actives%).
equip-pet
Looks up PetPlayerData for the executor, then bails with ACTIVE_PETS_LIMIT_REACHED if activePets().size() is already at the cap from PetsConfiguration.MaxPets#maxPets(player) (see permission overrides). If the pet is already active this is a silent no-op. Refuses with CANNOT_EQUIP_IDENTICAL_PET if another active pet shares the same blueprint id. On success, marks the pet active in PetPlayerData and fires PetEquipEvent — this action does not spawn the entity itself, that's spawn-pet's job.
unequip-pet
Silent no-op if the pet isn't currently active. Otherwise marks it inactive and fires PetUnequipEvent, which EntityListener listens to and removes the pet's spawned entity as a side effect.
spawn-pet
Looks up the pet in PetPlayerData; sends PET_NOT_FOUND if it isn't owned. Checks the WorldGuard pets flag at the player's current location first (PET_SPAWN_DENIED_HERE if denied), then delegates to EntityService#spawn(player, pet); a null result (event cancelled, or no free spot found) sends PET_SPAWN_CANCELLED. On success, sets the pet as the player's selectedPetId.
despawn-pet
Sends PET_NOT_FOUND if the pet isn't owned. Otherwise removes its spawned entity (if any) and clears selectedPetId regardless of whether an entity was found.
pickup-pet
Builds an ItemStack via PetService#buildItemStack(Pet) and mails it to the player through MailboxService#giveOrAdd. Only on confirmed delivery does it call PetPlayerData#removePet(pet) and send PET_REMOVED — a mailbox failure leaves the pet in the player's data untouched and sends UNEXPECTED_ERROR.
open-pet-container
Throws (uncaught RuntimeException, logged as an action failure) if the pet id doesn't resolve. Silently does nothing if pet.inventory().size() == 0 — i.e. the pet has no storage effect levelled up yet. Otherwise opens a PetInventoryMenu for it.
ride-pet
Throws if the pet id doesn't resolve to an owned pet or to a currently-spawned PetEntity — the pet must already be spawned (via spawn-pet or auto-respawn) before it can be ridden. Delegates to EntityService#ride, which applies the rideable-effect-level gate described above.
Preset actions
create-preset takes a PlaceholderWrapper<String> (the name); the other two take a PlaceholderWrapper<PetPreset> and must resolve to a PetPreset object, not a UUID string. All three throw if the executor has no loaded PetPlayerData. apply-preset fires PetUnequipEvent for every pet it deactivates and PetEquipEvent for every pet it activates, but does not go through equip-pet, so neither the active-pet cap nor the identical-blueprint rule is re-checked. See Pet Presets.
rename-pet
Takes two arguments and, unlike every other action here, the first is a PlaceholderWrapper<Pet> — it must resolve directly to a Pet object, not a UUID string. The second is a PlaceholderWrapper<String> for the new name. Validation: an empty name sends PET_RENAME_CANCELLED; a name longer than PetsConfiguration#maxPetNameLength (max-pet-name-length in config.yml, default 16) sends PET_NAME_TOO_LONG with a maxLength placeholder. On success the pet's Component name is recoloured to its blueprint's rarity color, PET_RENAMED is sent with oldName/newName placeholders, and if the pet is currently spawned its nametag is refreshed.
Registered ItemStack suppliers
AstralPets registers two ItemStackSupplier namespaces with AstralPaperAPI.registerItemStackSupplier, making pets and pet food resolvable anywhere AstralCore's CommonRegistries.itemStackSuppliers() registry is consulted:
Namespace | Class | Key format | Backing lookup |
|---|---|---|---|
|
|
|
|
|
|
|
|
The /give command (AstralCore's GiveItemCommand) resolves its <itemId> argument with Key.key(itemId) and matches it against every registered supplier's completions(), so both keys work there directly:
Inside an action, dialog, or menu, use the stacksuppliers complex placeholder (ItemStackSupplierPlaceholder) rather than the bare key string — a raw pets:simple literal does not resolve through PlaceholderWrapper<ItemStack> (ItemStackAdapter only accepts an ItemStackPlaceholder, an ItemStack, or an ItemStackWrapper as its input object — a plain String falls through to its AdapterDeserializationException default case), but a full %...% placeholder does, because ItemStackSupplierPlaceholder hands back an ItemStackPlaceholder object, not text:
Both suppliers' completions() are rebuilt on reload() — called once at startup after blueprints load, and again on /pets reload. keyOf(ItemStack) is the reverse lookup: it reads the stack's PDC and returns the pets:<id>/pets.food:<id> key, or null if the stack isn't one of this plugin's items.
Cross-server player data
PetSnapshotAdapter (key astralpets:player_data) is registered with SyncAPI in AstralPets#onEnable, making PetPlayerData a cross-server-synced snapshot for every online player:
Method | Description |
|---|---|
| Unmodifiable view of every owned |
| Unmodifiable, computed from |
| The pet tied to |
|
|
| Mutates the owned list; |
| Mutates the active-id list directly, bypassing the equip cap and identical-blueprint checks in |
| Unmodifiable view of the player's presets. |
| Mutates the preset list. |
| Lookups; the name form is case-insensitive. |
| Unequips every active pet (despawning it and firing |
| Wipes pets, active ids, and selection — what |
equippedSince on each active Pet is a relative offset while the player is offline: apply() (on data load) turns it back into an absolute timestamp, and unload() turns it back into a relative offset before the snapshot is persisted — so effects that scale with "time equipped" keep counting correctly across a server hop.
PetPlayerData itself implements ComplexPlaceholder (namespace pets, sub-keys pets, activePets, selectedPet, hasSelectedPet, presets), but the instance registered globally at root scope is a separate class, PetsPlaceholder — same namespace, different sub-keys (blueprints, equipped, actives, all, presets), reading the executing player's PetPlayerData internally. See Placeholders for the full, consolidated pets_*/pet_* surface.
Mutating PetPlayerData directly (as opposed to going through the actions above) bypasses the equip cap, the identical-blueprint check, and the equip/unequip events — reserve it for admin tooling.
Events
All events extend PetEvent (itself extending AstralCore's AbstractEvent extends org.bukkit.event.Event) and expose pet() — a Lombok fluent getter, no get prefix. Entity-level events additionally extend PetEntityEvent, adding entity() (the PetEntity).
High-level pet events
PetEquipEvent/PetUnequipEvent
Fired by the equip-pet/unequip-pet actions, after the pet's active state has already changed. Not cancellable — pure notifications.
Method | Return | Description |
|---|---|---|
|
| The pet that was (un)equipped. |
|
| The owner. |
EntityListener reacts to PetUnequipEvent by removing the pet's spawned entity, if any.
PetGainExperienceEvent
Fired by PetService#gainExperience before any level-up math runs. Async-flagged (isAsync() true). Not Cancellable, but mutable via Lombok @Getter @Setter on both fields — the caller re-reads experienceGained() immediately after callEvent(), so a listener that lowers it (or sets it to 0) directly reduces (or cancels) the XP actually granted.
Method | Return | Description |
|---|---|---|
|
| The pet gaining experience. |
|
| The amount about to be applied — mutate to change it. |
|
|
|
PetLevelUpEvent
Fired once per level gained inside the same gainExperience loop, async-flagged. Not Cancellable, but mutable: PetService reads newLevel() back and calls pet.level(levelUpEvent.newLevel()) — a listener can redirect which level the pet actually lands on.
Method | Return | Description |
|---|---|---|
|
| The pet levelling up. |
|
| The level before this step. |
|
| The level about to be applied — mutate to redirect it. |
PetConsumeEvent
Fired by PetListener#onConsume when a player right-clicks with a pet item (adding it to their owned pets — distinct from feeding food to a spawned pet, which is PetInteractEvent). Cancellable — cancelling leaves the item stack untouched and the pet is not added to PetPlayerData.
Method | Return | Description |
|---|---|---|
|
| The pet decoded from the clicked item's PDC. |
|
| The consuming player. |
|
| Standard |
PetSkinApplyEvent
Fired by SkinService#apply just before a skin is applied, after the pet has been checked to be skinless and to accept this skin. Cancellable — cancelling leaves the pet unskinned and the skin item in the player's hands. Applying is permanent, so this is the last point at which anything can stop it.
Method | Return | Description |
|---|---|---|
|
| The pet about to be skinned. |
|
| The player applying it. |
|
| The skin about to be applied. |
|
| Standard |
Entity-level events
PetSpawnEvent
Fired by EntityService#spawn, before the entity is created. Cancellable; cancelling makes spawn() return null.
Method | Return | Description |
|---|---|---|
|
| The pet about to be spawned. |
|
| The owner it will spawn for. |
|
| Mutable, and read back by |
|
| Standard |
PetDespawnEvent
Fired from PetEntity#remove(), async-flagged whenever removal doesn't happen on the primary thread. Not cancellable — the entity is already gone by the time this fires.
Method | Return | Description |
|---|---|---|
|
| The pet that was despawned. |
|
| The now-removed pet entity (still readable, just no longer in the world). |
EntityListener listens to this to unregister the entity from EntityService and send PET_DESPAWNED to the owner.
PetInteractEvent/PetAttackEvent
Fired by PetInteractListener off the raw PacketEvents INTERACT_ENTITY packet (main hand only, with a 150ms per-player cooldown): PetAttackEvent for the ATTACK action, PetInteractEvent for everything else. Both async-flagged, neither Cancellable.
Method | Return | Description |
|---|---|---|
|
| The pet interacted with. |
|
| The interacting player. |
|
| The pet entity that was clicked. |
EntityListener handles PetInteractEvent at LOW priority: if the clicker isn't the owner it's currently a no-op (TODO: Animation); if the owner is holding a matching food item, it feeds the pet (consuming 1 item, or the whole held stack while sneaking, capped so XP doesn't overshoot max level) and spawns heart particles; otherwise it opens the pet's menu. PetAttackEvent has no default listener in this plugin — it exists purely as an extension point.
PetMountEvent/PetDismountEvent
PetMountEvent is fired by EntityService#ride, scheduled onto the main thread, right before the player is added as a passenger. Cancellable — cancelling prevents the mount. PetDismountEvent is fired by EntityListener off Bukkit's EntityDismountEvent once the base entity confirms the dismount; not cancellable (a notification — the dismount already happened).
Method | Return | Description |
|---|---|---|
|
| The pet. |
|
| Its pet entity. |
|
| The rider. |
|
|
|
Custom pet effects
Pet effects implement PetEffect:
Implementations are @ConfigSerializable records so a blueprint's effects: list can deserialize straight into them via Configurate — e.g. MoneyPetEffect(Map<Integer, Double> values, Duration interval). The type: key in a blueprint's effect entry is looked up in PetEffectsRegistry:
PetEffectsRegistry.register(id, class) is public, and the three shop/jobs effects use it themselves from their hooks — so an effect type can be contributed from outside, as long as it is registered before the blueprints that name it are loaded (the hooks do so in onEnable, ahead of loadConfiguration()).
missingProvider(id) is how an unregistered id is told apart from one whose plugin is simply absent: ids in the optional-provider map are skipped with a warning during blueprint loading, everything else fails the file.
An effect only needs to supply values and apply/remove — how it reads is label + unit + a transformer, so a new type needs no display code.
See Pet Effects for what each built-in type id does in-game and its YAML shape.
WorldGuard integration
WorldGuardHook is only constructed (AstralPets#worldGuardHook) when the WorldGuard plugin is present at onLoad; it registers a custom state flag named pets (default ALLOW) with WorldGuard's FlagRegistry. If a flag named pets already exists under a different type, registration fails and a warning is logged — worldGuardHook()/WorldGuardHook.FLAG_INSTANCE stay usable regardless as long as the conflict didn't happen.
isPetsAllowed returns true whenever FLAG_INSTANCE is null (no WorldGuard, or a registration conflict) — the check only ever denies when the flag explicitly resolves to DENY at that location. Two call sites gate on it:
spawn-petchecks it directly at the player's location before callingEntityService#spawn, sendingPET_SPAWN_DENIED_HEREon denial.PetWorldGuardListenerre-checks it on everyPetSpawnEvent(ignoreCancelled = true) and cancels the event if denied — this is the actual enforcement chokepoint, since every spawn path (manual, on-join respawn, world change, region re-entry) routes through that event, not just the action.
A PetsWGSessionHandler (registered on WorldGuardHook#onEnable) additionally despawns a player's pet entities the moment they cross into a DENY region mid-session, and auto-respawns their selectedPet when they leave one — the movement itself is never blocked, only the pet entity is managed.
Nametag visibility
The pet's mob is a real, AstralMobs-spawned entity, so the server's own entity tracker handles it. What still needs managing is the nametag — a packet text display mounted on the mob — and that is what PetVisibilityTask does. It runs synchronously every 10 ticks (the pass reads live entity state, position and passengers, and sends packets derived from it, which is unsafe off the main thread) over EntityService#entities() and, per pet:
re-asserts the nametag mount, because a single
SetPassengerscan be dropped by a client that is not yet tracking the mob, or evicted by a vanilla passenger broadcast. While a player is riding, the tag is hidden and the real passenger list is re-asserted instead, so a client that missed the mount does not walk around believing it dismounted;removes viewers who went offline, changed world, or moved 64+ blocks away (destroy packet), and adds every online player inside that radius in the same world (spawn packet).
PacketEntity#addViewer/#removeViewer send those packets to one client each — there is no global broadcast.
Permission nodes
Node | Grants |
|---|---|
|
|
|
|
|
|
|
|
|
|
See Commands for the full per-subcommand breakdown.
Max active pets overrides
The equip cap enforced by equip-pet isn't a single fixed permission node — it's a config-driven map, max-active-pets in config.yml (see Configuration for the resolution rule in full):
PetsConfiguration.MaxPets#maxPets(Player) checks every key in permissions the player has, taking the highest matching value, and falls back to default if none match. The permission strings are whatever you configure — group.roturier above is this network's example, not a hardcoded node — so granting a higher pet cap to a rank is entirely a config + permission-plugin exercise, not a code change.