Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,20 @@ CI runs both backends — see [.github/workflows/ci.yml](.github/workflows/ci.ym

- `config` — `PluginConfig` wraps the YAML `FileConfiguration`. Treat it as immutable; `/vshop reload` builds a fresh instance.
- `locale` — `MessageManager` loads `lang/messages_<locale>.yml`, parses MiniMessage, and is the only place that emits player-facing text.
- `storage` — `StorageManager` owns the `DataSourceProvider` (Hikari) and exposes one repository per concern (`shops()`, `slots()`, `inventory()`, `transactions()`, `notifications()`, `limits()`, `coOwners()`, `playerCache()`, `playerPreferences()`). Repository implementations live under `storage/sqlite` and `storage/mysql`; the SQL-agnostic schema bootstrap is in `storage/repo/SchemaInitializer`. Cross-backend data movement is in `storage/migrate/MigrationService` (invoked by `/vshop migrate`).
- `storage` — `StorageManager` owns the `DataSourceProvider` (Hikari) and exposes one repository per concern (`shops()`, `slots()`, `inventory()`, `appearance()`, `transactions()`, `notifications()`, `limits()`, `coOwners()`, `playerCache()`, `playerPreferences()`). Repository implementations live under `storage/sqlite` and `storage/mysql`; the SQL-agnostic schema bootstrap is in `storage/repo/SchemaInitializer`. Cross-backend data movement is in `storage/migrate/MigrationService` (invoked by `/vshop migrate`).
- `economy` — `EconomyService` is the only caller of Vault. Fee/share math is centralized here; never call `Economy` directly elsewhere.
- `shop` — domain. `ShopRegistry` is the in-memory authoritative map of `UUID -> Shop`. `ShopService` is the lifecycle coordinator (create/load/delete, persistence + registry + villager state in lockstep). `ShopVillagerManager` handles the live entity (AI lock, invulnerability, respawn on chunk load, custom name regen). Subpackages mirror flows: `trade` (purchase/sell), `edit` (slot/menu editing), `coowner` (PRIMARY/MANAGER/STAFF), `egg` (spawn-egg crafting), `listener` (Bukkit events that fan into the services), `cache` (player-head/online cache).
- `shop` — domain. `ShopRegistry` is the in-memory authoritative map of `UUID -> Shop`. `ShopService` is the lifecycle coordinator (create/load/delete, persistence + registry + villager state in lockstep). Subpackages mirror flows: `entity` (the shop's in-world representation), `trade` (purchase/sell), `edit` (slot/menu editing), `coowner` (PRIMARY/MANAGER/STAFF), `egg` (spawn-egg crafting), `listener` (Bukkit events that fan into the services), `cache` (player-head/online cache).
- `shop/entity` — how a shop shows up in the world. `ShopEntityBackend` is the strategy interface (spawn / refresh / refreshDisplayName / remove); `VillagerBackend` owns the live Villager (AI lock, invulnerability, custom name regen, the `shop_id` PDC key) and `integration/fancynpcs/FancyNpcBackend` the packet NPC. `ShopEntityService` is the facade that resolves a shop to its backend from `ShopAppearanceRegistry` — **depend on the facade, not on a concrete backend**. The villager-specific escape hatches (`villagerKey()`, `findEntity()`) live on `VillagerBackend` and are reached via `ShopEntityService#villagers()`. `ShopDisplayName` renders the floating name for both backends (Bukkit wants a `Component`, FancyNpcs a MiniMessage `String`). `ShopEntityIntegration` is the lifecycle contract an optional rendering plugin implements.
- `ui` — `ui/dialog` is the BedrockDialog adapter (`DialogService`); `ui/chest` builds the inventory-based browse/edit/restock/player-picker UIs; `ui/text` renders chat output (history, search, list).
- `command` — `VShopCommand` builds the Brigadier tree and delegates per-subcommand classes in `command/sub`.
- `integration` — `MvshopPlaceholders` is an optional PAPI expansion, registered only when both the plugin is present and `placeholderapi.enabled` is true.
- `integration` — `MvshopPlaceholders` is an optional PAPI expansion, registered only when both the plugin is present and `placeholderapi.enabled` is true. `integration/fancynpcs` renders shops as FancyNpcs NPCs; it is the **only** place allowed to reference `de.oliver.fancynpcs.*`, and the composition root must never name a class in it outside the `isPluginEnabled("FancyNpcs")` guard (use `ShopEntityIntegration` for the held reference). Three facts drive its shape, all measured: NPCs are invisible to the Bukkit entity API, so nothing chunk- or entity-based applies; `NpcInteractEvent` is a synchronous Bukkit event, so no scheduler hop is needed; and `NpcData#setSkin` blocks the calling thread on a cache miss, so skins are warmed off-thread before spawning. NPCs are created with `saveToFile(false)` and rebuilt from `shop_appearance` at boot — our DB stays authoritative and `/vshop migrate` keeps carrying cosmetics.
- `api` — public surface registered to `ServicesManager` (`ModernVillagerShopAPI`); `api/price/PriceRegistry` is the extension point external plugins use to influence prices (read via `shop.trade.PriceResolver`).
- `item`, `model` — data carriers (item snapshots, enums, value objects).

### Cross-cutting invariants

- Persistence is repository-per-table. There is no ORM; each repository is hand-written SQL with a SQLite and a MySQL implementation. Schema differences are kept inside the backend-specific class, not branched in shared code.
- The Villager is the source of truth for "is there a shop here," but its position/customName are derived state regenerated from DB on load. Never mutate the entity directly — go through `ShopVillagerManager` or `ShopService`.
- The Villager is the source of truth for "is there a shop here," but its position/customName are derived state regenerated from DB on load. Never mutate the entity directly — go through `ShopEntityService` or `ShopService`.
- Co-owner role (`PRIMARY` / `MANAGER` / `STAFF`) gates *what an owner can do in their own shop*; the `modernvillagershop.*` permissions in [paper-plugin.yml](src/main/resources/paper-plugin.yml) gate *whether the command/feature is available at all*. `*.others` permissions bypass role for moderation.
- BedrockDialog callbacks may fire off the main thread. Anything that touches Bukkit API must be wrapped in `Bukkit.getScheduler().runTask(plugin, ...)` — see existing flows in `shop/trade/TradeFlow` and `shop/edit/SlotEditFlow` for the pattern.
- BedrockDialog only ships `ConfirmDialog` / `NoticeDialog` / `MultiButtonDialog` / `InputDialog` and has no `onClose` on Bedrock — design flows around explicit cancel buttons, not close detection. Sliders are banned for amount/price (use `InputDialog`).
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ModernVillagerShop replaces the vanilla trade window with a **chest UI + Dialog*
- **Co-owners with revenue sharing.** `PRIMARY` / `MANAGER` / `STAFF` roles per shop, percentage shares that always add up to 100%, instant payout split on every sale, and an ownership-transfer flow.
- **Trade limits.** Per-slot caps, counted per player or server-wide, with an optional rolling reset window. Remaining amount and time-to-reset are shown in the slot lore.
- **Villagers that stay put.** Shop villagers are AI-locked, invulnerable, protected from portals, and respawned from the database on chunk load if something removes them.
- **Custom shop appearance.** With [FancyNpcs](https://modrinth.com/plugin/fancynpcs) installed, a shop can render as an NPC instead of a villager — a player skin, any entity type, glow colour, scale and equipment — all through `/vshop appearance`, no UI to click through.
- **Spawn-egg based creation.** `/vshop egg` hands out an egg that encodes how many listing rows the shop gets — 1 to *n* rows, or unlimited.
- **Full trade history and statistics.** Filterable history (`--side`, `--from`, `--to`, `--player`, `--shop`), per-shop stats, cumulative fees, and audit fields (`basePrice` / `finalPrice` / `resolvedBy`) on every record.
- **Owner notifications.** Chat notification on each trade while online, a summary on next login while offline, toggleable per player.
Expand All @@ -32,6 +33,7 @@ ModernVillagerShop replaces the vanilla trade window with a **chest UI + Dialog*
| [BedrockDialog](https://modrinth.com/plugin/bedrockdialog) | **required** |
| Geyser + Floodgate | optional — needed only to serve Bedrock clients |
| [PlaceholderAPI](https://www.spigotmc.org/resources/placeholderapi.6245/) | optional |
| [FancyNpcs](https://modrinth.com/plugin/fancynpcs) | optional — lets shops render as NPCs instead of villagers |

## Installation

Expand Down Expand Up @@ -71,6 +73,7 @@ Root command is `/vshop` (bare `/vshop` prints help).
| `/vshop stats <shopId>` | Shop statistics | `modernvillagershop.stats` |
| `/vshop history [page] [--shop <id>] [--side sell\|buy] [--from <date>] [--to <date>] [--player <name>]` | Trade history | `history` / `history.others` |
| `/vshop edit [shopId]` | Open the owner/editor menu | `edit` / `edit.others` |
| `/vshop appearance <shopId> <sub>` | Change how the shop looks (needs FancyNpcs) | `edit.appearance` / `edit.others` |
| `/vshop coowner <shopId>` | Co-owner management UI | `coowner.manage` / `.others` |
| `/vshop transfer <shopId> <player>` | Transfer `PRIMARY` ownership | `coowner.transfer` / `.others` |
| `/vshop egg <player> <lines\|inf\|admin>` | Give a shop spawn egg | `egg` / `admin.egg` |
Expand All @@ -81,12 +84,33 @@ Root command is `/vshop` (bare `/vshop` prints help).

`--from` / `--to` accept `YYYY-MM-DD` or `YYYY-MM-DDTHH:mm[:ss]`, interpreted in the server's default time zone.

### `/vshop appearance`

Command-only, by design — this is an occasional operation with a lot of knobs, which reads better flat than as a menu tree.

| Subcommand | Effect |
| --- | --- |
| `show` | Print the current settings (read-only, so the console can run it too) |
| `npc [skin]` | Switch to a PLAYER NPC. Without `skin`, uses the owner's name |
| `villager` | Switch back to a plain villager |
| `type <entityType>` | Change the NPC's entity type |
| `skin <name\|uuid\|url\|@none> [slim]` | Set or clear the skin (PLAYER type only) |
| `glow <true\|false> [color]` | Glow and glow colour |
| `scale <n>` | Size multiplier |
| `equip <slot> [none]` | Equip the item in your hand, or clear the slot |
| `attribute <name> <value\|@none>` | Set a FancyNpcs attribute (e.g. `pose sitting`) |
| `reset` | Drop every override and go back to a villager |

Equipment slots are FancyNpcs': `MAINHAND`, `OFFHAND`, `HEAD`, `CHEST`, `LEGS`, `FEET`, `BODY`, `SADDLE`.

Appearance is stored in this plugin's own database, so it travels with `/vshop migrate` and NPCs are rebuilt from it on every boot. If FancyNpcs is missing or `fancynpcs.enabled` is false, NPC-backed shops fall back to villagers with a single warning — cosmetics never take a shop offline.

## Permissions

All nodes are prefixed `modernvillagershop.`. Two convenience bundles exist:

- **`modernvillagershop.player`** (default: everyone) — `use`, `egg`, `list`, `search`, `stats`, `history`, `open.nearby`, and the `edit.*` / `coowner.*` nodes needed to run your own shop.
- **`modernvillagershop.admin`** (default: op) — `admin.egg`, `admin.edit`, `admin.export`, `admin.import`, `edit.others`, `coowner.manage.others`, `coowner.transfer.others`, `history.others`, `open.any`, `migrate`, `reload`.
- **`modernvillagershop.admin`** (default: op) — `admin.egg`, `admin.edit`, `admin.export`, `admin.import`, `admin.appearance`, `edit.others`, `coowner.manage.others`, `coowner.transfer.others`, `history.others`, `open.any`, `migrate`, `reload`.

Two independent layers decide what a player can do:

Expand All @@ -95,6 +119,8 @@ Two independent layers decide what a player can do:

The `*.others` nodes are moderation overrides — they ignore role entirely and apply to any shop.

Two appearance nodes sit outside the bundles' defaults: `edit.appearance.url` (op) gates loading a skin from an arbitrary URL, since that pulls a remote image through the server, and `admin.appearance` (op) bypasses the `fancynpcs.allowedTypes` and `fancynpcs.maxScale` limits.

## Configuration highlights

Full annotated defaults live in [`src/main/resources/config.yml`](https://github.com/f0reachARR/ModernVillagerShop/blob/main/src/main/resources/config.yml).
Expand Down Expand Up @@ -162,6 +188,8 @@ which copies shops, listings, stock, transactions, notifications, limits, co-own
| `%mvshop_total_sales_<player>%` | Cumulative sales |
| `%mvshop_total_purchases_<player>%` | Cumulative purchases |

**FancyNpcs** (`fancynpcs.enabled: true`) — renders shops as NPCs; see [`/vshop appearance`](#vshop-appearance). Built against the `de.oliver:FancyNpcs` 2.x API. FancyNpcs 2.10.0+ requires Java 25 on the server; if you are on Java 21, use the `-java21` builds FancyNpcs publishes.

**Events** — `ShopCreateEvent`, `ShopDeleteEvent`, `ShopPreTransactionEvent` (cancellable), `ShopTransactionEvent`, `ShopSlotChangeEvent`.

**Public API** — `ModernVillagerShopAPI` is registered with Bukkit's `ServicesManager` and exposes shop lookup, search, statistics and history for dashboards, logging and third-party integrations.
Expand Down
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ repositories {
name = "placeholderapi"
url = "https://repo.extendedclip.com/content/repositories/placeholderapi/"
}
maven {
name = "fancyinnovations"
url = "https://repo.fancyinnovations.com/releases"
}
exclusiveContent {
forRepository {
maven {
Expand All @@ -40,6 +44,11 @@ dependencies {
compileOnly("me.clip:placeholderapi:2.11.6")
// BedrockDialog (Modrinth Maven). Version slug pinned to the latest available 1.21.8+ compatible build.
compileOnly("maven.modrinth:bedrockdialog:1.0.3")
// FancyNpcs API. Pinned to 2.9.2 — the last release whose API jar is a Java 17
// class file (2.10.0+ ship Java 25 bytecode, unreadable by our JDK 21 toolchain).
// 2.9.2's full public API is present unchanged in 2.10.x / 2.11.x, so binaries
// compiled here run against the current plugin releases.
compileOnly("de.oliver:FancyNpcs:2.9.2")
implementation("com.zaxxer:HikariCP:5.1.0")
runtimeOnly("org.xerial:sqlite-jdbc:3.49.1.0")
runtimeOnly("com.mysql:mysql-connector-j:9.1.0")
Expand Down
Loading
Loading