Astral Realms Documentation Help

Developer API

AstralPlayerShops exposes its state through two services hung off the main plugin instance (AstralPlayerShops.get()), three custom Bukkit events fired around shop creation and removal, and a SyncAPI snapshot adapter for the owner's cross-server recap preference. There is no static *API façade — external plugins reach everything through the plugin instance.

Maven coordinates

<dependency> <groupId>com.astralrealms</groupId> <artifactId>playershops</artifactId> <version>1.1-SNAPSHOT</version> <scope>provided</scope> </dependency>

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

Plugin handle

import com.astralrealms.playershops.AstralPlayerShops; AstralPlayerShops plugin = AstralPlayerShops.get(); ShopService shops = plugin.shops(); TransactionService transactions = plugin.transactions();

Custom Bukkit events

All three events live in com.astralrealms.playershops.event. PlayerShopCreateEvent and PlayerShopRemoveEvent share a common base, PlayerShopEvent, which extends AstralCore's AbstractEvent and carries the shop (PlayerShop) the event concerns.

PlayerShopPreCreateEvent

Cancellable, not a PlayerShopEvent (there is no PlayerShop yet at this point — the shop hasn't been built). Fired by ShopListener#onCreate the moment a sneak-right-click on an eligible container passes every placement check (not blacklisted, not already a shop, no block in front, town BLOCK_PLACE permission), but before the playershops-creation dialog opens.

Getter

Type

Value

getPlayer()

Player

The player attempting to create the shop.

getBlock()

Block

The clicked container block.

getLocation()

Location

block.getLocation().

getItemStack()

ItemStack

The item held in the player's main hand at the time of the click.

Cancelling this event silently aborts creation — the creation dialog never opens and no message is sent.

@EventHandler public void onPreCreate(PlayerShopPreCreateEvent event) { if (event.getItemStack().getType() == Material.BEDROCK) event.setCancelled(true); }

PlayerShopCreateEvent

Cancellable PlayerShopEvent. Fired synchronously on the main thread by ShopService#create, after the per-player shop limit and duplicate-location checks pass but before the shop is persisted to the database or its sign/hologram are created. The PlayerShop carried by getShop() already has its final uniqueId(), price, type, and location — it just hasn't been saved yet.

Getter

Type

Value

getPlayer()

Player

The creating player.

getBlock()

Block

The container block.

getLocation()

Location

block.getLocation().

getShop() (inherited)

PlayerShop

The shop about to be saved.

Cancelling sends PSMessages.CREATION_CANCELLED to the player and aborts the save — no row is written, no sign or hologram is created.

@EventHandler public void onCreate(PlayerShopCreateEvent event) { if (event.getShop().price() > 1_000_000) event.setCancelled(true); }

PlayerShopRemoveEvent

Not cancellable. A PlayerShopEvent constructed with isAsync = true, fired after the shop row has already been deleted from the database — it is a notification, not a gate. Called from both overloads of ShopService#delete:

Getter

Type

Value

getShop() (inherited)

PlayerShop

The shop that was just removed.

getPlayer()

Player (nullable)

The player who broke the container, when known. null for the admin/system delete path (ShopService#delete(PlayerShop, boolean), e.g. admin removal or plugin-driven cleanup).

getBlock()

Block (nullable)

The broken container block. null on the same admin/system path.

Because it fires async and after the fact, use it for logging/analytics rather than trying to veto the removal.

@EventHandler public void onRemove(PlayerShopRemoveEvent event) { getLogger().info(event.getShop().ownerName() + "'s shop was removed"); }

ShopService

plugin.shops(). Owns the in-memory shop cache (by UUID and by chunk), the buy/sell trade flow, and the sign/hologram lifecycle.

Method

Returns

Use

create(Player, Block, ItemStack, int price, ShopType, boolean townShared, boolean townEconomy)

CompletableFuture<Optional<PlayerShop>>

Validates the per-player limit and duplicate-location, fires PlayerShopCreateEvent, persists, and spawns the sign/hologram. Empty result on any failed check or a cancelled event.

update(PlayerShop)

CompletableFuture<PlayerShop>

Re-saves an already-existing shop (e.g. after editing price/type).

buy(Player, PlayerShop, Block)

void

Opens the playershops-buy container menu after checking the chest has stock.

buy(Player, PlayerShop, Block chestBlock, int amount)

void

Executes a purchase: checks stock, transfers funds via EconomyService (async), then re-checks stock atomically on the main thread before moving items, updating the sign, notifying the owner (if recapEnabled), and recording the transaction. Refunds the buyer if the post-transfer stock re-check fails, the transfer itself fails, or item movement throws.

sell(Player, PlayerShop, Block)

void

Opens the playershops-sell container menu after checking the chest has space and the player has items.

sell(Player, PlayerShop, Block chestBlock, int amount)

void

Executes a sale: checks chest space and player item count, transfers funds via EconomyService (async), then atomically re-checks both the player's items and the chest's space on the main thread before moving items (mailing back any that don't fit), updating the sign, notifying the owner, and recording the transaction. Refunds the seller on the same failure conditions as buy.

delete(Player, PlayerShop, Block)

CompletableFuture<Void>

Player-initiated removal (block break path): deletes the row, despawns the hologram, fires PlayerShopRemoveEvent, and deletes transaction history.

delete(PlayerShop, boolean giveItemsToPlayer)

CompletableFuture<Void>

Admin/system removal: deletes the row, despawns the hologram, optionally mails the chest's contents to the owner (or just breaks the block naturally), destroys the sign and chest, then fires PlayerShopRemoveEvent with null player/block.

findByLocation(Location)

Optional<PlayerShop>

Resolves the container at a location (also matches when the location is the sign, via the sign's facing).

findByBlock(Block)

Optional<PlayerShop>

Same resolution as findByLocation, starting from a Block.

findByUniqueId(UUID)

Optional<PlayerShop>

Cached lookup by shop UUID.

findByChunk(Chunk)

Set<PlayerShop>

Every cached shop in a chunk.

findNeighboringShop(Location)

Optional<PlayerShop>

Checks the four horizontal neighbors (N/E/S/W) of a location for a shop; used to reject adjacent-shop placement.

computeRecap(Player, long timestamp)

CompletableFuture<List<ShopRecap>>

Delegates to TransactionService#findRecapsByPlayer; returns an empty list (rather than a failed future) on error.

refreshShop(PlayerShop)

void

Re-resolves the shop's block on the main thread and re-renders its sign.

updateSign(PlayerShop, Block chestBlock)

void

Re-renders the sign attached to the given chest block from the current stock/space and config.yml: sign template.

AstralPlayerShops.get().shops() .findByBlock(clickedBlock) .ifPresent(shop -> { if (shop.hasPermission(player)) AstralPlayerShops.get().shops().refreshShop(shop); });

buy/sell and their -amount overloads are the same methods the buy-playershop-item and sell-playershop-item actions call — see Shops for the full trade flow, including the atomic stock/space re-check and refund-on-failure behavior.

TransactionService

plugin.transactions(). Owns the transactions table, a 5-minute-access-expiry Caffeine cache of per-player recap aggregates, and a background sweep that deletes transactions older than a week (runs every 12 hours).

Method

Returns

Use

recordTransaction(UUID shopId, Player buyer, int amount, double unitPrice)

CompletableFuture<Void>

Inserts a new ShopTransaction row (totalPrice = unitPrice * amount, timestamped now).

deleteByShopId(UUID shopId)

CompletableFuture<Void>

Bulk-deletes a shop's transaction history — called by ShopService#delete.

findTransactionsByShop(UUID shopId)

CompletableFuture<List<ShopTransaction>>

Full transaction history for one shop.

findStatsByShopId(UUID shopId)

CompletableFuture<ShopStats>

Daily/weekly earnings, transaction count, and item count for one shop.

findRecapsByPlayer(UUID playerId, long timestamp)

CompletableFuture<List<ShopRecap>>

Every trade across the player's shops since timestamp, grouped by ItemStack into one ShopRecap per item (summed count/earnings). Cached per playerId:timestamp key for 5 minutes.

AstralPlayerShops.get().transactions() .findStatsByShopId(shop.uniqueId()) .thenAccept(stats -> getLogger().info( "Shop " + shop.uniqueId() + " earned " + stats.weeklyEarnings() + " this week"));

Model classes

PlayerShop

@Entity("shops"), implements Unique and ComplexPlaceholder (namespace shop). Backed by an AtomicReference<ItemStack> lazily deserialized from the stored item blob on first itemStack() call (double-checked locking).

Field / accessor

Type

Notes

uniqueId()

UUID

@Id, column id.

ownerId()/ownerName()

UUID/String

Snapshotted at creation time.

price()

int

Per-item price; mutable via the generated setter.

material()

String

Base Material name — always the raw material, even for custom items.

itemStack()

ItemStack

Lazily deserialized from the stored item bytes; null if deserialization fails.

type()

ShopType

BUY or SELL; mutable.

x()/y()/z()/world()

int/int/int/String

Container location.

location()

Location

Convenience: rebuilds a Location from world/x/y/z.

serverId()

UUID

The network server that created the shop.

townShared()

boolean

Mutable; whether other town members with USE_PLAYER_SHOPS may trade.

economyAccount (field) / economyAccount()

UUID (nullable)

null unless town-economy routing is enabled; the accessor falls back to ownerId when the field is null.

createdAt()

long

@CreatedAt, stored as SQLAccessor.LONG_TIMESTAMP.

hasPermission(Player)

boolean

true for the owner, playershops.admin holders, or — if townShared — town members with USE_PLAYER_SHOPS at the shop's location.

get(PlaceholderContext) sub-keys (%shop_<key>%): id, owner, ownerName, price, material, type, typeFormatted, item, location, townShared, townEconomy. See Placeholders.

ShopTransaction

@Entity("transactions"), implements Unique, ComplexPlaceholder (namespace transaction), and Comparable<ShopTransaction> (sorted newest-first by createdAt).

Field

Type

uniqueId()

UUID (@Id, column id)

shopId()

UUID

buyerId()/buyerName()

UUID/String

quantity()

int

totalPrice()

double

createdAt()

long (@CreatedAt)

get(PlaceholderContext) sub-keys (%transaction_<key>%): id, buyer, amount, price, date.

ShopRecap

record ShopRecap(UUID shopId, ItemStack itemStack, ShopType type, int itemsCount, double earnings), implements ComplexPlaceholder (namespace entry). One instance per distinct item traded across a player's shops within the recap window — shopId is null on the aggregated instances TransactionService builds (the aggregate spans potentially many shops for the same item).

get(PlaceholderContext) sub-keys (%entry_<key>%): id, item, type, typeFormatted, count, earnings.

ShopStats

record ShopStats(double dailyEarnings, double weeklyEarnings, int dailyTransactions, int weeklyTransactions, int dailyAmount, int weeklyAmount), implements ComplexPlaceholder (namespace stats). Sub-keys mirror the record components 1:1 (%stats_dailyEarnings%, %stats_weeklyAmount%, etc.).

ShopType

public enum ShopType { BUY, SELL; public ShopType next() { ... } // cycles BUY -> SELL -> BUY }

BUY = the owner stocks the chest and sells to players; SELL = the owner buys from players. next() is what the creation/edit dialogs use to toggle the type. The ordinal (0 = BUY, 1 = SELL) is what's stored in the shops.type column.

Cross-server player data

PSSnapshotAdapter (key astralplayershops:data) is registered with SyncAPI in AstralPlayerShops#onEnable, making PSPlayerData a cross-server-synced snapshot for every online player. It implements UnloadableSnapshotAdapter<PSPlayerData>: create(Player) seeds a new player with recapEnabled = true, lastLogout = -1; unload(...) stamps lastLogout with the current time when the player fully disconnects (used to compute the away-recap window on their next join).

import com.astralrealms.playershops.storage.PSPlayerData; import com.astralrealms.sync.SyncAPI; SyncAPI.findData(player.getUniqueId(), PSPlayerData.class) .ifPresent(data -> { boolean wantsRecapPings = data.recapEnabled(); // gates the in-game "shop sale" chat ping long lastLogout = data.lastLogout(); // -1 if never recorded });

recapEnabled gates the real-time buy/sell chat notification sent to a shop owner (see ShopService#buy/#sell); lastLogout is what /ps internalRecap reads to compute the away-recap window. See Activity Recaps and AstralSync's Adapter Interfaces for the base contract.

Registered actions

AstralPlayerShops registers seven executable actions into AstralCore's action registry — six via registerAction (menu/dialog-scoped) and one via registerActionGlobally (toggle-playershops-recap). Any plugin's menus or dialogs can invoke them by name. See Actions for the full list with their argument shapes.

Database schema

Two tables, created from schema.sql on connect:

CREATE TABLE IF NOT EXISTS shops ( id UUID PRIMARY KEY, owner_id UUID NOT NULL, owner_name VARCHAR(255) NOT NULL, material VARCHAR(100) NOT NULL, item BLOB NOT NULL, price INT NOT NULL, type INT NOT NULL, x INT NOT NULL, y INT NOT NULL, z INT NOT NULL, world VARCHAR(255) NOT NULL, server_id UUID NOT NULL, town_shared BOOLEAN NOT NULL DEFAULT FALSE, economy_account UUID NULL DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS transactions ( id UUID PRIMARY KEY, shop_id UUID NOT NULL, buyer_id UUID NOT NULL, buyer_name VARCHAR(255) NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (shop_id) REFERENCES shops (id) ON DELETE CASCADE );

transactions.shop_id cascades on delete — removing a shops row (via either ShopService#delete overload) removes its transaction history at the database level, in addition to the explicit TransactionService#deleteByShopId call made from the same code path.

Last modified: 25 July 2026