Activity Recaps
A recap summarizes a shop owner's recent buy/sell activity across all of their shops: how many items moved through each shop, and how many coins the owner earned versus spent. There are two entry points — an automatic prompt shown to an owner who was away while their shops were traded at, and an on-demand /ps recap — and both compute over the same underlying transaction data via ShopService#computeRecap.
Player data: PSPlayerData
Recap state lives in a small AstralSync-backed record, com.astralrealms.playershops.storage.PSPlayerData:
Field | Type | Default | Description |
|---|---|---|---|
| boolean |
| Whether this player receives the away-recap prompt and real-time trade pings. Flipped by |
| long (epoch millis) |
| Timestamp of this player's last logout/unload. |
It is synced across the network by PSSnapshotAdapter (UnloadableSnapshotAdapter<PSPlayerData>) under sync key astralplayershops:data:
Method | Behavior |
|---|---|
|
|
| Fresh data for a first-ever load: |
| No-op — recap data is read on demand, nothing needs to be pushed onto the player at load time. |
| Stamps |
Away recap
RecapListener listens for AstralSync's PlayerDataLoadedEvent, which fires whenever a player's synced data has finished loading:
The elapsed-time gate is exactly System.currentTimeMillis() - playerData.lastLogout() >= 30_000L — a player who reconnects within 30 seconds of their last logout never sees the prompt, even with unseen activity. If any of the four conditions fails (no data, notifications disabled, lastLogout <= 0, or under 30 seconds elapsed) the listener returns silently; if the recap window has zero entries it also returns silently rather than sending an empty prompt.
RECAP_NOTIFICATION is sent with three placeholders registered on its container — %duration% (DurationParser.toHumanReadable of the elapsed time), %spent%, and %earned% (both NumberFormatUtils.formatWithSpaces, space-grouped whole numbers) — but the shipped messages.yml copy only interpolates %duration% in the away-message text itself; %spent%/%earned% are available to a customized message even though the default French copy doesn't reference them.
Clicking the message runs /ps internalrecap, a @Private subcommand (hidden from tab-completion) — see Commands — /ps internalRecap. It re-reads lastLogout from the player's PSPlayerData via SyncAPI.findData and recomputes the same way, opening the playershops-absent-recap dialog with the result. If lastLogout <= 0 at that point it sends RECAP_NOTHING_TO_SHOW instead.
On-demand recap: /ps recap
Any player can pull a recap of their own shops at any time with /ps recap (player-only — the handler takes a Player, not a CommandSender). Unlike the away recap, this one is not gated on recapEnabled — disabling notifications only silences the automatic prompts, not the manual command.
The lookback window is recap-interval from Configuration — a Duration (default 24h) read from config.yml; the command computes from System.currentTimeMillis() - recapInterval.toMillis() up to now. See Commands — /ps recap for the full command reference.
Recap computation
Both entry points run through ShopService#computeRecap(Player, long timestamp), which delegates to TransactionService#findRecapsByPlayer(UUID, long) and swallows any failure into an empty list (logging the error) rather than propagating it.
TransactionService caches results in a Caffeine AsyncLoadingCache<String, List<ShopRecap>> keyed by "<playerId>:<timestamp>", 5 minutes access-expiry. A cache miss runs TransactionRepository#findRecapsForPlayer, which sums transactions.total_price and transactions.quantity, joined to shops, filtered to shops.owner_id = <player> and transactions.created_at >= timestamp, grouped by shop/type/item. The service then re-groups those rows by ItemStack across all of the owner's shops into the final list — so the shopId on each returned ShopRecap is always null (it is not meaningful once entries for the same item from different shops have been merged).
ShopRecap (com.astralrealms.playershops.model.ShopRecap) is a ComplexPlaceholder under namespace entry, used to render each row inside the recap dialogs:
Field / sub-key | Type | Description |
|---|---|---|
| UUID | Always |
|
| The traded item. |
|
|
|
— → | Component |
|
| int | Total quantity traded for this item across the window. |
| double | Total coin value for this item across the window. |
At both call sites (RecapListener and PSCommand) the entries are further reduced into two totals: the sum of earnings() over entries where type() == BUY becomes earned, and the sum over entries where type() == SELL becomes spent — a BUY-type shop is one where the owner stocks and sells to players (revenue in), a SELL-type shop is one where the owner buys from players (coins paid out). See Placeholders for the full %entry_*% inventory alongside %shop_*% and %transaction_*%.
Toggling recap notifications
[toggle-playershops-recap] runs ToggleRecapNotificationAction, registered with registerActionGlobally (not registerAction) — unlike most of AstralPlayerShops' actions, it is not scoped to shop-menu contexts and can be referenced from any menu's actions: list network-wide (e.g. a general player-settings menu), not just AstralPlayerShops' own configs.
It looks up the executor's PSPlayerData via SyncAPI.findData and flips recapEnabled:
Data found, now enabled →
RECAP_ENABLED.Data found, now disabled →
RECAP_DISABLED.No data found for the player →
UNEXPECTED_ERROR.
See Actions for the full action reference.
recapEnabled gates two things, not just the away prompt: ShopService#buy/#sell also check the shop owner's recapEnabled before sending them a real-time BUY_NOTIFICATION/SELL_NOTIFICATION chat ping at trade time. Toggling this preference off silences both the live per-trade ping and the login recap prompt at once — see Shops.
Placeholder: %playershops_recapNotification%
PSPlaceholders (namespace playershops) exposes the player's current recap preference for use in menu toggle rendering (e.g. lighting up an on/off icon in a settings menu):
Placeholder | Returns |
|---|---|
|
|
See Placeholders for the full placeholder inventory.
Messages
Message | Sent when |
|---|---|
| Away-recap prompt on login. See Away recap. |
| After |
| Sent instead of opening a dialog when the computed recap has zero entries — from |
| Recap computation failed (logged), or |
| The two possible values of |