Skip to content
Merged
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
109 changes: 109 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,115 @@

## Unreleased

## Version 1.1.0

### New Features

#### Shops

+ Added admin shops: shops the server itself runs, set up entirely in game and opened by clicking an NPC.
+ `/tritown shop create <id>` makes one and drops you straight into the editor. Add an item by clicking it in your
own inventory or dragging it over the menu — your item stays where it is, and everything about it is kept, so a
renamed, enchanted or otherwise custom item is sold exactly as you made it.
+ The order entries are in is the order players see. Right-click one in the editor to pick it up and click where it
belongs — on any page — or send it straight to the front or the back. A whole shop can also be sorted by item
name or by price in one go.
+ An entry can be sold, bought back, or both. Left-click buys one, shift-left-click buys as many as you can afford
and carry, right-click sells one, and shift-right-click sells everything you are carrying.
+ A price can be money, items, or both at once, and so can a payout — so a shop can sell for currency, barter, or
ask for a fee alongside the materials.
+ How many items one purchase hands over is set per entry, and is not capped at a stack: a bundle of 128 bread is
handed over as two stacks.
+ An entry can have a stock that refills on a timer, a per-player limit that resets daily, weekly or never, or
neither. Both are shown on the item, counting down as players buy.
+ Entries and whole shops can be locked behind a permission node or a standing in Towny — being in a town or a
nation, or being a mayor or a king. A locked entry either greys out with the reason or is hidden entirely.
+ Town and nation members can be given a discount, set under `shops.discounts`. Discounts do not stack; the best
one applies, and the menu shows the saving.
+ A purchase above `shops.confirm-above` asks for confirmation first, so a mis-click cannot empty an account.
+ Every purchase and sale is recorded in the transaction log and shows up in `/eco history` as a shop movement,
naming the shop it happened at.
+ `/tritown shop stats <id>` shows what a shop has traded and how much currency it has taken in and paid out, per
entry and in total.
+ Shops open from a [FancyNpcs](https://modrinth.com/plugin/fancynpcs) NPC. Bind one with
`/tritown shop bind <id> <npc>` and clicking it opens the shop. The binding follows the NPC rather than its name, so
renaming it in FancyNpcs does not break anything. FancyNpcs is optional — without it everything else still works, and
`/tritown shop open <id> [player]` still opens a shop from the console or for testing.

#### Admin Panel

+ Added `/tritown admin`, an administration panel that opens as a menu. It is the way in to what an owner needs to read
about the server, starting with the economy; more sections will follow.
+ The economy panel puts the whole economy on one screen:
+ **Money supply** — how much currency exists, how it splits between player wallets, town and nation banks and the
server's own accounts, how far it has moved over the window, and how much that is per wallet. The supply is
measured off the accounts themselves rather than added up from movements, so it is exact.
+ **Faucets and sinks** — how much currency was created and how much was removed, each broken down by what caused
it: new players, shops, Towny, administrators or another plugin. The net says which way the economy is drifting,
per day and as a share of the supply, with how long it would take at that rate to double or run dry.
+ **Wealth distribution** — the median, mean and largest wallet, the share held by the richest tenth, and an
inequality figure with a word for what it means.
+ **Circulation** — how much money players moved between themselves, over how many payments, and how quickly the
supply turns over.
+ **A chart** — the window drawn as seven columns, each as tall as its net change, so a payday, a sink nobody uses
or a runaway faucet shows up as a shape rather than a number.
+ **Accounts, the richest accounts, the shops and the ledger's own settings**, so nothing needs a command to check.
+ Every figure can be read over the last day, the last week, the last month or everything on record. Click the clock to
change the window and the whole screen follows it.
+ A full breakdown lists every source of money and every kind of account with what it created, removed and netted, for
the same window.
+ The shop sales figures now have a home in the panel: one screen lists every shop with what it has taken in and paid
out, and clicking one opens the figures that shop already had. `/tritown shop stats <id>` still opens a single shop
directly, and shift-clicking a shop here opens its editor.
+ Each section has its own permission — `tritown.admin` to open the panel, `tritown.admin.economy` and
`tritown.admin.shops` for the sections — so a moderator can be given the reading without the editing.

### Fixes

#### Misc

+ Fixed items being draggable into a plugin menu. Clicks were already blocked, but a drag across the menu was not.

### Technical Details

#### Economy

+ The economy now keeps figures of its own, hour by hour: what was created and destroyed, what for, who held it, and a
measurement of the ledger taken on every flush. They live in `plugins/TriTown/economy/statistics.json` and are
written on the same interval as balances, so a crash costs at most one interval of them and never a balance.
+ `economy.stats.enabled` turns the figures off entirely, and `economy.stats.retention-days` says how far back they
reach — 30 days by default, or 0 to keep them forever.
+ Recording a movement costs no disk and no lock: the counters are plain adders, which matters because Towny moves
money from its own threads.
+ Transaction statistics are kept even when `economy.history.enabled` is off, since they cost nothing per transaction.

#### Shops

+ Added [FancyNpcs](https://modrinth.com/plugin/fancynpcs) as an optional dependency. TriTown builds and runs without
it; the parts that need it simply stay off.
+ Shops are stored in `plugins/TriTown/shops/shops.json`, written atomically with a backup copy in the same way
balances are. A shop you edit is saved immediately; stock levels and sales figures are written every
`shops.save-interval` seconds.

#### Misc

+ GUIs can now handle a drag through `onDrag`, which cancels the drag by default. The GUI manager also forgets a player
who quits with a menu open.
+ Paged GUIs gained `PagedLayout.FRAMED`, which insets the content and draws a border around it, and `navButtons`,
which puts a menu's own actions in the fixed navigation row instead of after the last item where they move as the
list grows. `contentIndex` turns a clicked slot into a position in the item list, which a framed layout needs.
+ `PlayerData` and `ServerData` gained `getJsonObject`, so a nested object that was written can be read back.
+ Added `ChatPrompt`, which asks a player a question in chat and hands the answer back on the server thread. Menus use
it for anything that has to be typed, such as a price or a permission node.
+ `GUIManager.openLater` opens a menu on the following tick, which is what a menu reached by clicking inside another
one needs so the server and the client do not disagree about what is on screen.

#### Economy

+ `EconomyUtil.withdraw` and `EconomyUtil.deposit` can now name the source and reason of a movement, so a feature no
longer has to reach past them for its transactions to be recorded as anything but an anonymous Vault call.


## Version 1.0.0

### New Features
Expand Down
81 changes: 69 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ overview.
| Platform | Paper API 26.2 (MC 26.2) |
| Towny | 0.103.2.7 (`towny_version` in gradle.properties) |
| Vault API | 1.7.1 (`vault_api_version` in gradle.properties) |
| FancyNpcs API | 2.9.2 (`fancynpcs_version`, API artifact only) |
| Java toolchain | JDK 25 |

## After Every Change: Keep the Changelog and Docs in Sync
Expand Down Expand Up @@ -57,30 +58,32 @@ Before finishing any task that changes the plugin, do all of the following:
```

The local test server lives in `run/` (gitignored). `copyPlugin` puts the matching Towny jar in `run/plugins/`, but the
Paper 26.2 jar (`run/paper-*.jar`) and Vault must be downloaded by hand, and `eula.txt` accepted, before `startServer`
works. No economy plugin is needed — TriTown supplies the economy itself. The server console reads commands from the
terminal running Gradle.
Paper 26.2 jar (`run/paper-*.jar`), Vault, and FancyNpcs (Maven carries its API only) must be downloaded by hand, and
`eula.txt` accepted, before `startServer` works. No economy plugin is needed — TriTown supplies the economy itself.
The server console reads commands from the terminal running Gradle.

## Repository Layout

```
src/main/kotlin/net/trilleo/mc/plugins/tritown/
├── Main.kt # Plugin entry point (Main.instance, Main.reload())
├── commands/ # Sub-commands (auto-registered)
│ ├── info/
│ └── moderation/
│ ├── admin/ economy/ info/
│ └── moderation/ scoreboard/ shop/
├── config/ # PluginConfig (typed config.yml wrapper), EconomySettings
├── data/ # JSON-persisted PlayerData / ServerData and their managers
├── economy/ # The economy: ledger, accounts, currencies, Vault provider, storage (not scanned)
├── enums/ # AccountType, DisplayLocation, FillMode, PagedGUIMode, ProviderMode, TransactionType
├── guis/ # GUIs (auto-registered, extend PluginGUI / PagedPluginGUI)
├── economy/ # The economy: ledger, accounts, currencies, Vault provider, statistics,
│ # storage (not scanned)
├── enums/ # AccountType, FlowCategory, StatsWindow, TransactionType, FillMode, …
├── guis/ # GUIs (auto-registered, extend PluginGUI / PagedPluginGUI); admin/ is the panel
├── items/ # Custom items (auto-registered, extend PluginItem)
├── listeners/ # Event listeners, including Towny events (auto-registered)
├── recipes/ # Recipes (auto-registered, implement PluginRecipe)
├── registration/ # Auto-registration engine (do not modify lightly)
├── shops/ # Admin shops: model, trading, storage, FancyNpcs bridge (not scanned)
├── tasks/ # Scheduled tasks (auto-registered, extend PluginTask)
└── utils/ # Lang, EconomyUtil, itemStack DSL, MessageUtil, LoreUtil, CountdownUtil, TeamUtil,
# TagUtil, PDCUtil, GameRuleUtil
└── utils/ # Lang, EconomyUtil, itemStack DSL, MessageUtil, LoreUtil, ChatPrompt, CountdownUtil,
# TeamUtil, TagUtil, PDCUtil, GameRuleUtil
src/main/resources/
├── config.yml plugin.yml
└── lang/ # en_US.yml, zh_CN.yml — every player-facing string
Expand All @@ -90,8 +93,8 @@ src/main/resources/

The plugin uses `PackageScanner` to discover components at startup — you **never** edit `plugin.yml` or wire things
manually. Just extend the right base class and place the file in the correct package. Packages outside the table below
are never scanned, which is why the economy core lives in `economy/`: it has to be alive in `onLoad`, long before the
registrars run.
are never scanned, which is why the economy core lives in `economy/` and the shop core in `shops/`: both have to be
alive before the registrars build the commands and menus that read them.

| Component | Base Class | Package |
|:------------|:-------------------------------|:------------------------|
Expand Down Expand Up @@ -172,6 +175,25 @@ complete stack and no separate economy plugin is needed. See
- **Charge before acting** — call `EconomyUtil.withdraw` and only perform the action when it returns `true`; refund with
`deposit` if the action then fails. Never check `has` and withdraw separately. Use `EconomyUtil.transfer` for a
payment between two accounts, which is atomic on TriTown's own economy.
- **Wire every new way money moves into the statistics.** The admin panel's figures are only as true as the
attribution behind them, and a movement nothing claims is filed as "Other plugins" — so a new faucet or sink that
skips this quietly makes the economy unreadable. For **every** feature that moves money:
1. **Move it through `EconomyUtil`** (or `EconomyService` inside the economy itself), never by writing a balance:
that path is the only one `EconomyService.record` — and therefore `EconomyPulse` — ever sees.
2. **Attribute it.** Use the four-argument `EconomyUtil.withdraw`/`deposit` with an `EconomyContext.SOURCE_*` and a
`TransactionReason` key, or wrap the work in `EconomyContext.with`. Add the `money.reason.*` key to both
language files.
3. **Check `FlowCategory.of` covers that reason.** If the feature is a faucet or a sink in its own right — a job
payout, a daily reward, a repair fee, a lottery — give it a `FlowCategory`, spell out its `money.flow.*` key in
both language files, and map the reason to it. A real faucet must never land in `OTHER`.
4. **Decide whether the panel should name it.** The full breakdown picks a new category up on its own; a card of
its own in `EconomyPanelGUI` is for a source worth watching separately.
See [Economy Statistics](docs/DEVELOPER_GUIDE.md#economy-statistics).
- **Never total raw reason strings** — a reason carries arguments (`money.reason.admin-set?admin=Bob`), so summing by
reason grows a row per player. `FlowCategory` is the grouping, and `FlowCategory.of` is the only place the mapping
lives.
- **The supply is measured, never accumulated** — `EconomyPulse.sample` walks the ledger on the flush task. Never keep
a running total of how much currency exists; it would drift the first time anything moved money unrecorded.
- **A failure carries a key, not a sentence** — `EconomyResult.Failure` holds a `money.error.*` translation key, and the
code that shows it picks the language. Those values are plain text, because Vault hands them straight to other
plugins, which print them verbatim; TriTown's own commands colour them with `common.error`.
Expand All @@ -190,6 +212,41 @@ complete stack and no separate economy plugin is needed. See
double-count every town deposit.
- **Costs and rewards are configurable** — put amounts in `config.yml`, not in Kotlin.

## Working with the Admin Panel

The panel in `guis/admin` is where an owner reads the server; `/tritown admin` opens it. See
[Admin Panel](docs/DEVELOPER_GUIDE.md#admin-panel).

- **A section is a card and a menu.** Adding one means adding a card to `AdminPanelGUI` and a menu of its own; nothing
else in the panel changes. Give it its own permission under `tritown.admin.*` and do not draw a card the viewer
cannot open.
- **The panel reads, it does not write.** Anything that changes the server belongs in the command or menu that owns it,
not here.
- **Format through `PanelRender`** — money, percentages, rates, timestamps and the cards themselves, so the same figure
reads the same wherever it appears. Amounts stay in minor units until they reach it.
- **The window belongs to the viewer**, in `PanelState`, so every menu of the panel agrees on what is being looked at.

## Working with Shops

**Shops are the server's own, not a player's.** See [Shops](docs/DEVELOPER_GUIDE.md#shops).

- **Go through `ShopManager`** — it is the only thing that reads or writes a shop. `save()` for a change to a
definition, which must never be lost; `markDirty()` for stock and statistics, which `ShopSaveTask` flushes.
- **Trade only through `ShopTrade`** — its ordering is what keeps a trade safe: everything that can refuse is asked
before anything is taken, and anything taken is remembered so it can be put back. Never charge and hand over in two
places.
- **Serialize items with `ItemCodec`** — Paper's byte form is the only round-trip that keeps every data component, so a
custom item survives. Never describe an item field by field.
- **Read Towny through `ShopAccess`** — it is the one place shops touch Towny, and it re-reads on every check.
- **Keep FancyNpcs isolated** — only `listeners/shop/ShopNpcListener` may name a FancyNpcs type in a signature, and
only `shops/npc/FancyNpcsAdapter` may touch the API. `ShopNpcBridge` exposes plain types so a server without the
plugin still loads everything else. Adding a FancyNpcs type to its signatures would take the shop command with it.
- **Attribute money with the four-argument `EconomyUtil.withdraw`/`deposit`** so a trade is recorded as a shop movement
rather than an anonymous Vault call.
- **Shop and entry names are administrator-written MiniMessage stored in the shop file**, not translation keys. Escape
anything player-written before embedding it; a shop's own name is deliberately not escaped, because an administrator
wrote it.

## Versioning & Releases

- `plugin_version` in [gradle.properties](gradle.properties) is the single source of truth for the plugin version. It
Expand Down
Loading