Astral Realms Documentation Help

Developer API

AstralShop exposes buy/sell eligibility and pricing to other plugins through the AstralCore ShopService SPI, lets other plugins grant per-player price adjustments through the static ShopAPI façade, and fires cancellable Bukkit events around every purchase and sale.

Maven coordinates

<dependency> <groupId>com.astralrealms</groupId> <artifactId>shop</artifactId> <version>1.0.1-SNAPSHOT</version> <scope>provided</scope> </dependency>

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

ShopService

ShopService is an AstralCore SPI interface (com.astralrealms.core.paper.service.ShopService extends AstralService) implemented by PaperShopService and registered on AstralPaperAPI when AstralShop enables:

// AstralShop#onEnable AstralPaperAPI.registerService(ShopService.class, this.shops);

Other plugins resolve it without depending on any AstralShop class:

import com.astralrealms.core.paper.AstralPaperAPI; import com.astralrealms.core.paper.service.ShopService; AstralPaperAPI.getService(ShopService.class).ifPresent(shopService -> { boolean buyable = shopService.canBeBought(itemStack); double price = shopService.calculateBuyPrice(player, itemStack, 1); });

Method

Returns

Notes

canBeBought(ItemStack itemStack)

boolean

true if any loaded shop has an item with buyable = true whose display is isSimilar to the stack.

canBeSold(ItemStack itemStack)

boolean

Same check against sellable items.

calculateBuyPrice(ItemStack itemStack, int quantity)

double

item.buyPrice() * quantity for the first matching buyable item across all shops. No per-player modifiers. Returns 0 if nothing matches.

calculateSellPrice(ItemStack itemStack, int quantity)

double

item.sellPrice() * quantity for the first matching sellable item. No per-player modifiers. Returns 0 if nothing matches.

calculateBuyPrice(Player player, ItemStack itemStack, int quantity)

double

Same lookup, then runs the result through ModifierService for that player's BUY-scoped modifiers. Returns 0 if nothing matches.

calculateSellPrice(Player player, ItemStack itemStack, int quantity)

double

Same, for SELL-scoped modifiers. Returns 0 if nothing matches.

All six methods scan every currently loaded shop's items and match by ItemStack#isSimilar, not by shop/item id — the same rule the /sellhand command uses to find what a held item sells as.

ShopAPI

ShopAPI (com.astralrealms.shop.ShopAPI) is a static utility, initialized in AstralShop#onEnable right after the services are constructed. It is the only supported way for another plugin to grant, revoke, or query a player's price modifiers.

Method

Returns

Notes

addModifier(UUID playerId, ShopModifier modifier)

void

Stores the modifier under its own Key, in memory, for that player. Adding a modifier with a Key that's already present replaces the existing entry.

removeModifier(UUID playerId, Key key)

void

No-op if the player has no modifier under that key.

hasModifier(UUID playerId, Key key)

boolean

false if the player has never had a modifier added (no map entry yet).

import com.astralrealms.shop.ShopAPI; import com.astralrealms.shop.model.modifier.ShopModifier; import com.astralrealms.shop.model.modifier.ShopModifierType; import com.astralrealms.shop.model.modifier.ShopModifierScope; import net.kyori.adventure.key.Key; // Grant a 10% discount on every buy, until removed Key key = Key.key("myplugin", "vip_discount"); ShopAPI.addModifier(player.getUniqueId(), new ShopModifier(key, ShopModifierType.RELATIVE, ShopModifierScope.BUY, -0.1)); // ... later ShopAPI.removeModifier(player.getUniqueId(), key);

Modifiers

A ShopModifier (com.astralrealms.shop.model.modifier.ShopModifier) is a plain data class:

public class ShopModifier { private final Key key; private final ShopModifierType type; private final ShopModifierScope scope; private final double value; }

Two subclasses narrow which items a modifier applies to; a bare ShopModifier applies to everything matching its scope across every shop:

Class

Extra field

Applies to

ShopModifier

Every item in every shop, subject only to scope.

ShopCategoryModifier(Key key, ShopModifierType type, ShopModifierScope scope, String categoryId, double value)

categoryId

Every item within the shop whose id equals categoryId.

ShopItemModifier(Key key, ShopModifierType type, ShopModifierScope scope, double value, String itemId)

itemId

Only the item whose id equals itemId (regardless of shop), subject to scope.

ShopModifierType

Value

Effect

RELATIVE

Contributes to a summed scalar applied as finalValue * (1 + scalar).

ABSOLUTE

Added directly to the running total before the relative scalar is applied.

ShopModifierScope

Value

Matches

SELL

Only sell-price computations.

BUY

Only buy-price computations.

BOTH

Not itself matched against — ModifierService#computePrice filters by exact scope equality against BUY or SELL, so a modifier must be created with BUY or SELL to take effect on that operation.

Price formula

ModifierService#computePrice filters the player's stored modifiers to those that:

  1. Have scope equal to the transaction's scope (BUY or SELL).

  2. Are not a ShopCategoryModifier with a different categoryId than the shop being bought/sold from.

  3. Are not a ShopItemModifier with a different itemId than the item being bought/sold.

The surviving modifiers are then folded into the base price (item.buyPrice() or item.sellPrice(), already multiplied by the transaction amount) by ModifierService#computeModifiers:

finalPrice = (base + sum of matching ABSOLUTE values) * (1 + sum of matching RELATIVE values)

ABSOLUTE values accumulate into the running total first; RELATIVE values accumulate into a separate scalar that scales the whole total (base plus absolutes) at the end. Order of modifier registration does not matter — only the final sums do.

Storage and lifecycle

Modifiers are held entirely in memory, in a ConcurrentHashMap<UUID, PlayerModifiers> inside ModifierService, keyed by player UUID and then by the modifier's own Key (so re-adding a modifier under the same Key overwrites the previous one). They are not persisted to disk or through AstralSync. ModifiersListener clears a player's entire modifier map on PlayerQuitEvent:

@EventHandler public void onQuit(PlayerQuitEvent e) { this.plugin.modifiers().clearModifiers(e.getPlayer().getUniqueId()); }

Any modifier granted by another plugin only lasts for the remainder of that play session unless the granting plugin re-applies it (e.g. on PlayerJoinEvent) every time the player reconnects.

Events

ShopEvent (com.astralrealms.shop.event.ShopEvent) is an abstract, cancellable Bukkit event (extends AbstractCancellableEvent implements Cancellable) that both concrete shop events extend:

Accessor

Type

Notes

getPlayer()

Player

The buyer/seller.

getConfiguration()

ShopConfiguration

The shop the item belongs to.

getItem()

ShopItem

The item being bought/sold.

getAmount()

int

The quantity involved.

getPrice()/setPrice(double)

double

The computed price (post-modifiers). Mutable — overriding it changes what's actually charged/paid.

isCancelled()/setCancelled(boolean)

boolean

Standard Bukkit Cancellable.

ShopBuyEvent

Fired by PaperShopService#buy after the item/eligibility checks and price computation, but before the EconomyService#withdraw call and before the item (or buy-actions) is granted. Cancelling it stops the purchase entirely — no funds are withdrawn and nothing is given.

ShopSellEvent

Fired by PaperShopService#sell after eligibility/inventory checks and price computation, but before EconomyService#deposit, before the items are removed from the seller's inventory, and before sell-actions run. Cancelling it stops the sale entirely.

import com.astralrealms.shop.event.ShopBuyEvent; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; public class ShopGuard implements Listener { @EventHandler public void onBuy(ShopBuyEvent event) { if (event.getConfiguration().id().equals("wood") && event.getAmount() > 64) { event.setCancelled(true); return; } // Charge a flat surcharge on top of the computed price event.setPrice(event.getPrice() + 5.0); } }

Both events construct with isAsync = false, so handlers always run on the main thread and are safe to touch other Bukkit API from.

ShopItem type serializer

ShopItem is registered as a Configurate TypeSerializer<ShopItem> (ShopItemTypeSerializer) via registerTypeSerializer in AstralShop#onEnable:

this.registerTypeSerializer(ShopItem.class, ShopItemTypeSerializer.INSTANCE);

The serializer only implements deserialize — it reads an item's node key as its id, buy-price, sell-price, display, buyable, sellable, give-item, buy-actions, and sell-actions, and derives shopId by walking up to the enclosing shop file's id node (node.parent().parent().node("id")). serialize is unimplemented and throws UnsupportedOperationException("Serialization of ShopItem is not supported.")ShopItem values can only be read from config, never written back out through Configurate. See Shop Configuration for the full YAML shape this serializer reads.

Where to go next

  • Overview — how shops, items, and modifiers fit together.

  • Shop Configuration — the ShopConfiguration/ShopItem YAML shape.

  • Commands/shop, /sellall, /sellhand.

  • Actions[buy-shop-item], [sell-shop-item], [sell-all].

  • Placeholders%shop_*%/%item_*%, including the modifier-aware %item_buyPrice%/%item_sellPrice% sub-keys.

Last modified: 25 July 2026