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 packet-level |
|
| Open the main pet menu, a specific pet's menu, or the pokedex menu. |
|
| AstralCore's generic dialog container, defaulting to |
|
| The loaded |
|
| The loaded |
|
| 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 packet entity for the player first, then fires PetSpawnEvent — a null return means either the event was cancelled or no collision-free spot was found within 5 blocks up/down of the target location. 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.
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 8 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 the packet entity for an owned pet at the player's location. | |
| Removes the pet's packet 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 packet entity (if spawned) 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.
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 |
| 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), but the instance registered globally at root scope is a separate class, PetsPlaceholder — same namespace, different sub-keys (blueprints, equipped, actives, all), 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 |
Entity-level events
PetSpawnEvent
Fired by EntityService#spawn, before the packet 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-dead packet entity (still readable, just no longer visible to anyone). |
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 packet 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 packet 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:
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.
Packet entity visibility
Pet entities are pure PacketEvents constructs (PacketEntity/PetEntity), not real Bukkit entities, so visibility is manual: EntityService tracks every spawned PetEntity in a single map, and PetVisibilityTask (async, every 10 ticks) walks that collection, removing viewers who went offline, changed world, or moved more than 64 blocks away (viewers().remove → destroy packet), and adding any online player within that radius in the same world (addViewer → spawn packet). PacketEntity#addViewer/#removeViewer send the actual spawn/destroy packets to just that one client — 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.