Skip to content
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,81 @@

## Unreleased

## Version 1.2.0

### New Features

#### Trading

+ Added player-to-player trading. Shift-right-click another player, or run `/trade <player>`, and once they accept you
both get the same table: up to sixteen stacks and any amount of money a side, yours on the left and theirs on the
right, each of you reading it in your own language.
+ Click an item in your inventory to put it up and click it again in the menu to take it back; right-click puts up
a single one. The gold ingot is your money — left-click adds, right-click takes off, shift does ten times as
much, and **Q** types an exact amount in chat. You can never put up more than you actually have.
+ What you put up leaves your inventory and is held by the trade, so what the other side is looking at cannot be
spent, dropped or deposited behind their back. It all comes straight back the moment the trade ends any way
other than going through.
+ Anything either of you changes clears both confirmations and greys the buttons for a moment, so nothing can be
swapped out after the other person has agreed to it. When you have both confirmed the items change hands and any
difference in money is paid across in one payment, recorded in the transaction log and counted in the admin
panel like any other payment between players.
+ Closing the menu, walking too far apart, disconnecting or the server stopping all call the trade off and hand
everything back. `player-trades.distance` sets how close you have to be, and `player-trades.request-expiry` how
long an unanswered request stands; `player-trades.enabled` turns the whole thing off.
+ NPCs wearing a player's shape are left alone, so shift-right-clicking a shop keeper still opens its shop.

#### Shops

+ An entry can now limit how much each player **sells** to the shop per day, per week or ever, alongside the limit on
how much they buy. The two are set separately in the entry editor and counted separately, so an entry can be "buy 64
a day, sell 256 a day" without one side spending the other's allowance.
+ Added a global shop, opened from anywhere with `/trades` and needing no NPC — for the goods the server always
trades. It is created empty on first start, appears in `/tritown shop list` and the editor like any other shop, and
is set up the same way; `shops.global-id` chooses which shop it is. It cannot be deleted while it is the one
`/trades` opens, and anyone may run the command, so what each player sees inside it is still the shop's own
permission and Towny requirements.
+ Shift-left-clicking anything that stacks now opens a menu to pick how many to buy: 1, 8, 16, 32 or 64. They are
priced at the entry's own rate, so eight of something sold sixteen at a time costs half of what the shelf quotes,
and an amount you cannot take is greyed out with the reason rather than refusing once you have clicked it. This
replaces "buy as many as you can", which gave you a number you had not chosen and no way to ask for a smaller one.

### Improvements

#### Shops

+ An entry's description now separates what it costs from what clicking does, with the prices, the payout and what is
left of the stock and your limits each in a block of their own.
+ A price that asks for items is refused for part of a purchase rather than quietly rounded, and says how many the
entry is traded at a time.
+ A price is now red and a payout green wherever they appear, including the items either side asks for, so buying and
selling can be told apart at a glance.

### Fixes

#### Shops

+ A stock and a per-player limit are now counted in items rather than in purchases, so they mean what they say. An
entry selling 16 at a time with a limit of 10 used to hand over 160 items, and a click spent one of the ten whether
it moved one item or a hundred and twenty-eight. A limit of 64 is now sixty-four items, and stays sixty-four if you
change the bundle afterwards. Existing shops are converted on first start, so every entry keeps trading exactly as
it did; only the number you see in the editor changes unit.
+ A shop's sales figures count items too, so what one entry has traded can be compared with another whatever their
bundles are. Existing figures are converted with everything else.

### Technical Details

#### Shops

+ An older shop file is now brought forward by `ShopMigrations` as it is read rather than being misread against the
current shape. Schema 2 is stock, limits and sales figures in items, plus the selling limit.

#### Misc

+ Handing a player items, asking whether they would fit, and adding lines to an item's lore are now shared utilities
— `InventoryUtil` and `LoreUtil.withLore` — rather than living inside the shop package, so anything else that moves
items or draws a menu decides both the same way shops do.

## Version 1.1.0

### New Features
Expand Down
28 changes: 24 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ src/main/kotlin/net/trilleo/mc/plugins/tritown/
├── 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, ChatPrompt, CountdownUtil,
# TeamUtil, TagUtil, PDCUtil, GameRuleUtil
├── trades/ # Player trades: sessions, escrow, the swap (not scanned)
└── utils/ # Lang, EconomyUtil, InventoryUtil, 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 @@ -93,8 +94,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/` and the shop core in `shops/`: both have to be
alive before the registrars build the commands and menus that read them.
are never scanned, which is why the economy core lives in `economy/`, the shop core in `shops/` and the trade core
in `trades/`: each has to be alive before the registrars build the commands and menus that read it.

| Component | Base Class | Package |
|:------------|:-------------------------------|:------------------------|
Expand Down Expand Up @@ -247,6 +248,25 @@ The panel in `guis/admin` is where an owner reads the server; `/tritown admin` o
anything player-written before embedding it; a shop's own name is deliberately not escaped, because an administrator
wrote it.

## Working with Player Trades

**A trade holds items that belong to a player.** See [Player Trades](docs/DEVELOPER_GUIDE.md#player-trades).

- **Items are escrowed, money is not.** An item leaves the player's inventory the moment it is put up, so the other
side can trust what it sees. A balance is read by everything else on the server, so money is only named on the table
and moves at settlement — which is why confirming re-checks it and the swap can still refuse over it.
- **Escrow goes back exactly once.** `TradeManager.cancel` is the only way a trade ends without the swap, and
`TradeSession.end` makes it idempotent. A new way for a trade to end is a new caller of `cancel`, never a new place
that drains an offer.
- **Hand items back inside `PlayerQuitEvent`.** It still runs before the server writes the player's inventory, which
is the whole reason a disconnect costs them nothing. Pass the leaving `Player` in rather than looking it up.
- **Every change to an offer goes through `TradeSession.touch`**, which drops both confirmations and starts the lock.
A confirmation must only ever describe the table as it was at the moment it was given.
- **The swap lives in `TradeExchange` and nowhere else** — everything that can refuse is asked before anything is
handed over, and money settles as a single net payment so a trade can never be half paid for.
- **Both windows are drawn for their viewer**, never for a side, and anything that changes the table calls
`TradeGUI.redraw` so the two can never disagree.

## Versioning & Releases

- `plugin_version` in [gradle.properties](gradle.properties) is the single source of truth for the plugin version. It
Expand Down
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,18 @@ and off with `/tt scoreboard`, and it takes turns with Towny's own plot HUD rath
**Shops the server runs.** Admin shops, set up entirely in game: click an item in your own inventory to put it on the
shelf and it is sold exactly as you made it, custom name, enchantments and all. An entry can be sold, bought back, or
both, and priced in currency, items, or a mix of the two. Give it a stock that refills on a timer, a limit on how much
each player may buy per day or per week, a permission node, or a requirement to be in a town or a nation — and give
each player may buy or sell per day or per week, a permission node, or a requirement to be in a town or a nation — and give
town or nation members a discount while you are at it. Players reach a shop by clicking a
[FancyNpcs](https://modrinth.com/plugin/fancynpcs) NPC, and every sale is recorded in the transaction log and totalled
in a sales view.

**Trading, player to player.** Shift-right-click another player, or run `/trade <player>`, and once they agree you
both get the same table: sixteen stacks and any amount of money a side, yours on the left and theirs on the right.
Items leave your inventory the moment you put them up and are held by the trade, so what the other side is looking at
cannot be spent behind their back, and anything changing on the table clears both confirmations — nothing can be
swapped out after somebody has agreed to it. Everything comes straight back if either of you closes the menu, walks
away or disconnects.

**An admin panel.** `/tt admin` opens a menu that reads the server back to you. The economy section shows how much
currency exists and who holds it, what created it and what removed it — new players, shops, Towny, administrators or
another plugin — with the net drift per day, how unevenly wealth is spread, how fast money circulates, and a chart of
Expand Down Expand Up @@ -91,6 +98,8 @@ Prebuilt jars are attached to every [GitHub release](https://github.com/Trilleo/
| `/baltop [page]` | List the richest accounts |
| `/eco <action> …` | Administer balances (OP only) |
| `/tt scoreboard` | Show or hide the sidebar |
| `/trades` | Open the server's global shop |
| `/trade <player>` | Ask another player to trade |
| `/tt shop <action> …` | Set up the server's shops (OP only) |
| `/tt admin [section]` | Open the admin panel (OP only) |

Expand All @@ -104,6 +113,10 @@ viewing someone else's history additionally needs `tritown.economy.admin.history
`unbind <npc>` to put an NPC behind the counter, and `stats <id>` for what it has traded. Each action has its own
permission, `tritown.shop.admin.<action>`. Players have no shop command of their own — they click an NPC.

`/trade` also takes `accept [player]` and `deny [player]`, which the request message offers as buttons. Both players
have to be within `player-trades.distance` blocks of each other, and have to stay that close for as long as the menu
is open. There is no permission node: whether players may trade at all is `player-trades.enabled`.

`/tt admin` opens the panel itself, and `economy` or `shops` opens that section directly. Opening the panel needs
`tritown.admin`; the sections need `tritown.admin.economy` and `tritown.admin.shops` on top of it.

Expand Down Expand Up @@ -147,9 +160,13 @@ version stays available as `/tritown:balance` and so on.
| `economy.stats.retention-days` | `30` | How far back those figures reach; `0` keeps them forever |
| `shops.enabled` | `true` | Turn shops off entirely |
| `shops.save-interval` | `60` | Seconds between writing stock and sales figures; edits are saved immediately |
| `shops.global-id` | `trades` | The shop `/trades` opens; created empty if missing, and not deletable |
| `shops.confirm-above` | `1000.0` | Purchase total that asks for confirmation first; `0` never asks |
| `shops.sell-rate` | `0.5` | What the editor suggests as a payout, as a fraction of the buy price |
| `shops.discounts.<standing>` | `0.0` | Money off for `has-town`, `has-nation`, `is-mayor` or `is-king` |
| `player-trades.enabled` | `true` | Turn player-to-player trading off entirely |
| `player-trades.distance` | `10.0` | How close two players must be to trade, and stay while the menu is open |
| `player-trades.request-expiry` | `60` | Seconds an unanswered trade request stands |
| `scoreboard.enabled` | `true` | Turn the sidebar off entirely |
| `scoreboard.refresh-interval` | `2` | Seconds between redraws of a sidebar nothing has changed on |
| `scoreboard.default-on` | `true` | Whether a player who has never used `/tt scoreboard` sees one |
Expand Down Expand Up @@ -182,9 +199,17 @@ A shop is created with `/tt shop create <id>`, which opens its editor. Everythin
- **Pricing it.** An entry has a buy side and a sell side, and each may be switched on or off on its own. Either side
can ask for money, for items, or for both at once. Money is typed in chat when you click the price; items are added
by clicking them in your inventory, and the stack size is the quantity.
- **Reaching it.** A shop normally stands behind an NPC. One does not: the shop named by `shops.global-id`
(`trades` by default) opens from anywhere with `/trades`, for the goods the server always trades. It is created
empty on first start, is edited like any other shop, and cannot be deleted while it is the one `/trades` opens.
- **Buying it.** A player left-clicks an entry to buy one purchase of it, and shift-left-clicks anything that stacks to
pick an amount instead — 1, 8, 16, 32 or 64, priced at the entry's own rate, so eight of something sold sixteen at a
time costs half. An amount they cannot take is greyed out with the reason rather than refusing after the click. Right
-click sells one purchase back, and shift-right-click sells everything they are carrying.
- **Limiting it.** *Stock* is shared by everybody and refills to full on a timer. A *limit* is per player and resets
daily, weekly, or never. Both are optional, and an entry with neither is unlimited, which is what an admin shop
usually wants.
daily, weekly, or never; buying and selling have one each, and they are counted separately. All of them are counted
in items rather than in purchases — a limit of 64 on an entry that sells 16 at a time is four purchases — and all of
them are optional. An entry with none is unlimited, which is what an admin shop usually wants.
- **Locking it.** A shop, and each entry inside it, can require a permission node or a standing in Towny — being in a
town, being without one, being in a nation, being a mayor or being a king. A locked entry shows the reason by
default, or can be hidden entirely.
Expand All @@ -205,6 +230,22 @@ Shops live in `plugins/TriTown/shops/shops.json`, written atomically with a `.ba
written straight away; stock levels and sales figures are written every `shops.save-interval` seconds, so a crash costs
at most that long of counters and never a shop.

### Player trades

Shift-right-click the other player, or run `/trade <player>`. They get a request with **Accept** and **Deny** buttons,
and nothing opens until they take it. Both of you have to be standing close by, and have to stay there.

In the menu, click an item in your inventory to put it up — right-click puts up a single one — and click it again in
the menu to take it back. The gold ingot is your money: left-click adds, right-click takes off, hold shift for ten
times as much, and press **Q** to type an exact amount in chat. You can never put up more than you actually have.

Anything either of you changes clears both confirmations and greys the button for a moment, so nothing can be swapped
out after the other person has agreed to it. When you have both confirmed, the items change hands and any difference
in money is paid across in one payment, recorded in the transaction log like any other.

Closing the menu calls the trade off and everything goes straight back. So does walking too far apart, disconnecting,
or the server stopping.

### The sidebar

Each board under `scoreboard.boards` has a `priority`, a `condition`, and a list of `lines`. A player sees the
Expand Down
Loading