Zurück zum Blog

22. September 2026 · Offensichtlicher

Transaktionen im GUI anzeigen

 

Economy logs · abstraction, drawn

Reason to
ItemStack

84 rows→1 filter→4 renderers→54 slots

Every economical transaction writes one row. The row answers who, how much, when — and, in one free-form string, why. Turning that string back into something a player can look at is the whole job. Here is the machine that does it.

SELECT * FROM economy_logs WHERE uuid =ORDER BY created_at DESC

transactions

ALL ACCOUNTS
user v Δ reason

rendering

JVM
TransactionRenderer.GLOBAL#create(reason)
idle — waiting for a reason
no candidate yet
chain of responsibility · first match wins

player view

GENERIC_54
Economy Log
0 / 50 rendered
01 · what gets written

One row per transaction, versioned per user

-- the balance lives elsewhere; this table is the ledger transactions(!user, !version, delta, reason, timestamp) -- ! := primary key ⇒ composite key (user, version)

version counts up per user, so the pair (user, version) is unique without a sequence, and gaps are impossible: row v84 is that account's 84th movement, full stop. delta is relative — never a balance — so replaying the table from zero reconstructs every account, and a lost write shows up as a hole in the numbering.

// the only way in withdraw(uuid, amount, reason) deposit(uuid, amount, reason)

The caller — a shop plugin, a plot manager, an admin command — supplies the reason. The economy never invents one. That single decision is the abstraction: the ledger stays plugin-agnostic, and each plugin keeps its own syntax, as long as that syntax carries enough context to trace the row back to the event that caused it.

02 · scoping the view

The ledger is server-wide. The GUI is one player's.

-- the chest holds 54 slots. the table holds everyone. SELECT * FROM economy_logs WHERE uuid = '5e10b791-…' ORDER BY created_at DESC LIMIT 54

Without the predicate, the first 54 rows of a busy server are four accounts' worth of noise and nobody's history. The filter is not a convenience on top of the view — it is what makes the view mean anything. Swap the account in the query bar above and the same 84-row ledger produces four completely different chests.

Where it sits matters. Filtering in SQL is one index scan on (uuid, created_at); filtering after the fetch means pulling the whole table across to throw most of it away. The LIMIT then has to match the container — 54 slots, 54 rows — or the last page silently disappears.

What it does not touch: the renderer. create(reason) never sees a uuid, a timestamp or a delta — only the string. Which account you scope to changes which reasons arrive, never how one is resolved. That separation is why the filter could be added here, late, without opening a single renderer.

03 · the free-form field

First token names the source. Everything after it is context.

Nothing validates these strings. Streetshop packs its context into colon-separated segments; the admin command uses plain words; plot-merge appends chunk coordinates in brackets it made up itself. Three plugins, three grammars, one column.

Which is exactly why rendering can't be a switch. The renderer that understands streetshop: is the same code that wrote it — it lives in that plugin and registers itself. The economy only knows how to ask.

And when no one claims a string — rewards:, pay to, and the BUY/SELL rows all come from plugins that never registered a renderer — the chain runs out and the fallback returns a question mark. An unknown reason is still a rendered reason, and the GUI never has a hole in it.

04 · inside the machine

Two types, one linked list, no registry

TransactionRenderer static TransactionRenderer GLOBAL; − Map<String, Renderer> cache; − TransactionRenderer next; + supports(String reason): boolean + create(String reason): Render + add(TransactionRenderer renderer) Render − String reason; − ItemStack result; // what the GUI shows
// resolution: walk until someone says yes reason ─▸ GLOBAL#create ├─ AdminCmd… ✗ ─next─▸ ├─ Streetshop… ✗ ─next─▸ ├─ PlotMerge… ✓ └─ ∅ unknown (terminal) // registration: plugins push themselves on TransactionRenderer.GLOBAL.add(new PlotMergeRenderer());

GLOBAL is a head node that supports nothing and delegates everything. Adding a plugin means one add() call at startup — no enum to extend, no switch to find, no central list of known sources. The economy module never learns that street shops exist.

cache keys the finished Render by reason string — not by row, not by account. Scope the query bar to e2539420-… and watch the ADMIN_CMD ADD VollmilchZiege burst resolve once and replay free for sixteen more rows: the chain walk is skipped, the same ItemStack is handed back. The cache outlives the filter, because the filter was never part of the key.

05 · the sample

All 84 rows, both queries merged

The server-wide ledger. Rows outside the current filter are dimmed; the icon column is not stored anywhere at all — it is what the chain produced.

  uuid version relative_change reason created_at
economy logs · chain of responsibility · generic_54 84 transactions · 4 accounts · 20 days

Komm mit der 1.21.4 - 26.2

→ totorix.net

Über uns

Wir bieten stabilen Citybuild an. Aktuell fokussieren wir uns auf die gestaltung einer fairen Wirtschaft und progressivem Gameplay.
We are not affiliated with Mojang AB.

Nützliche Links

Support

Wenn du Hilfe brauchst, kannst du uns gerne auf Discord kontaktieren.

Discord

This checkout process is operated by our online reseller & Merchant of Record, Tebex Limited, who also handles order-related inquiries and refunds.