Astral Realms Documentation Help

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

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

Repository: https://maven.astralrealms.fr/repository/maven-public/.

Plugin handle and services

import com.astralrealms.pets.AstralPets; AstralPets plugin = AstralPets.getPlugin(AstralPets.class);

AstralPets is annotated @Getter (Lombok, fluent — no get prefix), so every field below is a same-named accessor:

Getter

Type

Use

plugin.blueprints()

BlueprintService

Look up pet blueprints by id, list all loaded ones, build item stacks.

plugin.foodBlueprints()

FoodBlueprintService

Look up food blueprints by id, build/identify food item stacks.

plugin.pets()

PetService

Build a pet ItemStack, read a Pet back off PDC, grant experience.

plugin.entities()

EntityService

Spawn/despawn/find the packet-level PetEntity for a Pet, mount a player on one.

plugin.menus()

MenuService

Open the main pet menu, a specific pet's menu, or the pokedex menu.

plugin.dialogs()

DialogContainer

AstralCore's generic dialog container, defaulting to dialogs/*.yml. AstralPets ships no dialogs/ folder and no code path calls dialogs(), so it is currently wired but unused — the rename-dialog text block in config.yml is likewise not read by anything in this plugin's source.

plugin.configuration()

PetsConfiguration

The loaded config.yml record (see Configuration).

plugin.rarities()

RaritiesConfiguration

The loaded rarities.yml record.

plugin.itemSupplier()

PetsItemSupplier

The pets ItemStackSupplier registered with AstralPaperAPI.

plugin.foodItemSupplier()

PetsFoodItemSupplier

The pets.food ItemStackSupplier registered with AstralPaperAPI.

plugin.worldGuardHook()

WorldGuardHook

null when WorldGuard isn't installed — see WorldGuard integration.

BlueprintService/FoodBlueprintService

plugin.blueprints().findById("simple"); // Optional<PetBlueprint> — the shipped example blueprint plugin.blueprints().ids(); // Collection<String>, every loaded blueprint id plugin.blueprints().all(); // Collection<PetBlueprint> plugin.blueprints().cachedPets(); // List<Pet> — one max-level Pet per blueprint (pokedex) plugin.foodBlueprints().findById("apple"); // Optional<FoodBlueprint> — the shipped example food plugin.foodBlueprints().ids(); // Set<String>

PetService

ItemStack stack = plugin.pets().buildItemStack(pet); // existing Pet instance -> ItemStack ItemStack fresh = plugin.pets().buildItemStack(blueprint); // fresh Pet at UUID.randomUUID() -> ItemStack plugin.pets().fromItemStack(stack); // Optional<Pet> — reads the pet PDC entry back off a stack plugin.pets().isPetItem(stack); // boolean — cheap PDC presence check plugin.pets().gainExperience(player, pet, petEntity, 25.0); // grants XP, may trigger level-ups

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

PetEntity entity = plugin.entities().spawn(player, pet); // at the player's current location PetEntity entity = plugin.entities().spawn(player, pet, location); // explicit location plugin.entities().findByPetId(petId); // Optional<PetEntity> plugin.entities().findByPlayer(player); // Set<PetEntity> — every packet entity owned by this player plugin.entities().findByPassenger(player); // Optional<PetEntity> — the pet the player is currently riding plugin.entities().entities(); // Collection<PetEntity> — every spawned pet entity, server-wide plugin.entities().ride(player, pet, entity); // mount, subject to the rideable effect gate below

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.

plugin.menus().openMainMenu(player); // "pets-main" plugin.menus().openPetMenu(player, pet); // "pet-menu", with the pet passed as a menu parameter plugin.menus().openPokedexMenu(player); // "pokedex"

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

equip-pet

pet-id

Marks an owned pet active.

unequip-pet

pet-id

Marks an active pet inactive.

spawn-pet

pet-id

Spawns the packet entity for an owned pet at the player's location.

despawn-pet

pet-id

Removes the pet's packet entity, if any.

pickup-pet

pet-id

Converts an owned pet back into a mailed item and removes it from PetPlayerData.

open-pet-container

pet-id

Opens the pet's storage inventory (no-op if it has no storage).

ride-pet

pet-id

Mounts the player on the pet's currently-spawned entity.

rename-pet

pet name

Renames a pet, resolving the Pet object itself, not its id.

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

- "[equip-pet] %pet_id%"

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

- "[unequip-pet] %pet_id%"

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

- "[spawn-pet] %pet_id%"

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

- "[despawn-pet] %pet_id%"

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

- "[pickup-pet] %pet_id%"

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

- "[open-pet-container] %pet_id%"

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

- "[ride-pet] %pet_id%"

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

- "[rename-pet] %parameters_pet% %parameters_name%"

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

pets

PetsItemSupplier

pets:<blueprint-id>

BlueprintService#findById-> PetService#buildItemStack(PetBlueprint)

pets.food

PetsFoodItemSupplier

pets.food:<food-blueprint-id>

FoodBlueprintService#findById-> FoodBlueprintService#buildItemStack

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:

/give Steve pets:simple /give Steve pets.food:apple 4

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:

actions: - "[give-item] %stacksuppliers_pets_simple%" - "[give-item] %stacksuppliers_pets.food_apple%"

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:

import com.astralrealms.pets.storage.PetPlayerData; import com.astralrealms.sync.SyncAPI; SyncAPI.findData(player.getUniqueId(), PetPlayerData.class) .ifPresent(data -> { List<Pet> owned = data.pets(); // every pet the player owns List<Pet> active = data.activePets(); // equipped pets (effects applied) Optional<Pet> selected = data.selectedPet(); // the one currently spawned/spawnable boolean has = data.isActive(petId); boolean isSameBlueprint = data.isBlueprintActive(blueprint); });

Method

Description

pets()

Unmodifiable view of every owned Pet.

activePets()

Unmodifiable, computed from activePetsIds — pets currently equipped.

selectedPet()

The pet tied to selectedPetId (the one spawned / to be spawned), if any.

findById(UUID)

Optional<Pet> lookup by unique id within this player's owned pets.

addPet(Pet)/removePet(Pet)

Mutates the owned list; removePet also unmarks it active and deselects/despawns it if it was selected.

markAsActive(UUID)/markAsInactive(UUID)

Mutates the active-id list directly, bypassing the equip cap and identical-blueprint checks in equip-pet.

clear()

Wipes pets, active ids, and selection — what /pets reset calls.

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

pet()

Pet

The pet that was (un)equipped.

player()

Player

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

pet()

Pet

The pet gaining experience.

experienceGained()/experienceGained(double)

double

The amount about to be applied — mutate to change it.

totalExperience()/totalExperience(double)

double

pet.experience() + experienceGained at construction time (not automatically kept in sync if experienceGained is changed afterward).

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

pet()

Pet

The pet levelling up.

oldLevel()/oldLevel(int)

int

The level before this step.

newLevel()/newLevel(int)

int

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

pet()

Pet

The pet decoded from the clicked item's PDC.

player()

Player

The consuming player.

isCancelled()/setCancelled(boolean)

boolean

Standard Cancellable contract.

Entity-level events

PetSpawnEvent

Fired by EntityService#spawn, before the packet entity is created. Cancellable; cancelling makes spawn() return null.

Method

Return

Description

pet()

Pet

The pet about to be spawned.

player()

Player

The owner it will spawn for.

location()/location(Location)

Location

Mutable, and read back by PetWorldGuardListener (see below) — but not re-read by EntityService#spawn itself, which keeps using its own original location parameter for the actual spawn position. Changing location() in a listener currently affects the WorldGuard flag check only, not where the entity ends up.

isCancelled()/setCancelled(boolean)

boolean

Standard Cancellable contract.

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

pet()

Pet

The pet that was despawned.

entity()

PetEntity

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

pet()

Pet

The pet interacted with.

player()

Player

The interacting player.

entity()

PetEntity

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

pet()

Pet

The pet.

entity()

PetEntity

Its packet entity.

player()

Player

The rider.

isCancelled()/setCancelled(boolean)

boolean

PetMountEvent only.

Custom pet effects

Pet effects implement PetEffect:

public interface PetEffect { void apply(Player player, Pet pet); void remove(Player player, Pet pet); Map<Integer, Double> values(); default double levelValue(int level) { /* values().getOrDefault(level, 0.0) */ } default double value(int level) { /* sum of values() at keys <= level */ } default boolean async() { return true; } // apply/remove run off-thread unless overridden }

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:

public class PetEffectsRegistry { private static final NamedRegistry<Class<? extends PetEffect>> REGISTRY = new NamedRegistry<>(); static { REGISTRY.register("stats", StatPetEffect.class); REGISTRY.register("money", MoneyPetEffect.class); REGISTRY.register("potion", PotionPetEffect.class); REGISTRY.register("storage", StoragePetEffect.class); REGISTRY.register("rideable", RideablePetEffect.class); REGISTRY.register("no-fall", NoFallPetEffect.class); REGISTRY.register("shop-modifier", ShopModifierPetEffect.class); REGISTRY.register("jobs-modifier", JobModifierPetEffect.class); REGISTRY.register("rotating-shop-modifier", RotatingShopModifierPetEffect.class); } public static Optional<Class<? extends PetEffect>> findById(String id) { ... } }

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.

WorldGuardHook.isPetsAllowed(player, location); // boolean — true if WorldGuard absent, flag unset, or ALLOW

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-pet checks it directly at the player's location before calling EntityService#spawn, sending PET_SPAWN_DENIED_HERE on denial.

  • PetWorldGuardListener re-checks it on every PetSpawnEvent (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

pets.give

/pets give <player> <blueprint>.

pets.give.food

/pets food <player> <blueprint>.

pets.reset

/pets reset <player>.

pets.reload

/pets reload.

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):

max-active-pets: default: 1 permissions: group.roturier: 2

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.

Last modified: 25 July 2026