Arcadia Auction House

Arcadia Auction House adds a cross-player marketplace with item listings, a mailbox for delivered purchases, and Numismatics currency support. Requires Arcadia Lib.

File Details

arcadia-ah-1.3.0

  • R
  • May 19, 2026
  • 129.73 KB
  • 3.3K
  • 1.21.1
  • NeoForge

File Name

arcadia-ah-1.3.0.jar

Supported Versions

  • 1.21.1

Curse Maven Snippet

NeoForge

implementation "curse.maven:arcadia-ah-1493501:8116331"
Curse Maven does not yet support mods that have disabled 3rd party sharing

Learn more about Curse Maven

## [1.3.0] - 2026-05-20 (latest)

### Added

- **Shulker box cross-server safety (NBT round-trip guard)** — Server now validates that any item submitted to a listing survives a full serialize → deserialize → field-level diff before the row is committed. The `DataComponents.CONTAINER` (shulker boxes, bundles, modded containers) is compared by non-empty slot count, catching the long-standing bug where a shulker bought on a different server would arrive empty because the buyer's registry could not resolve some of the contained items. Refused listings cost nothing — the seller keeps both the item and the listing fee. Implemented as `AuctionItemSerializer.validateRoundTrip(...)`, gated by the new `config/integrity.strict_nbt_validation` flag (default `true`).
- **Featured listings (paid promotion)** — Sellers can pay an upfront extra fee (`featured_fee_percent`, default 30% of price) to promote a listing for `featured_duration_hours` hours (default 24, capped at the listing's natural expiry). Featured listings sort first in `getFiltered`. Two entry points: `/arcadia_ah sell <price> [qty] featured` lists with the upgrade applied immediately, and `/arcadia_ah feature <listingId>` upgrades an existing listing. Promotion is race-safe: the fee is refunded with an explanatory chat line if the listing was bought / expired in the moment between the click and the DB write. New `AuctionListing` fields (`featured`, `featuredUntil`) persisted in both MySQL (`featured`/`featured_until` columns) and singleplayer SavedData.
- **Global chat broadcasts on every new listing** — When `config/broadcast.enabled = true` (default), every successful listing fires a server-wide chat line `§6[AH] <seller> listed <item> for <price>!`. Players personalise the wording via `/arcadia_ah sellmessage set <template>` using placeholders `{seller}`, `{item}`, `{price}`. Templates are sanitised: control characters AND vanilla `§`-codes are stripped before broadcast to prevent chat-injection abuse — the `{item}` / `{price}` placeholders still carry their proper `Component` formatting through. Anti-spam: per-seller cooldown (`per_seller_cooldown_seconds`, default 30s). New table `arcadia_ah_player_settings(player_uuid PRIMARY KEY, sell_template, updated_at)`. Templates cached in-memory per UUID and evicted on logout.
- **Personal sales history (`/arcadia_ah history [page]`)** — Reads from the existing `arcadia_prestige_auction_sales_log`, paginated (`config/history.page_size`, default 10/page; `config/history.max_days` look-back window, default 30). Each row: date, buyer name (resolved via online lookup → profile cache → short UUID), item type, amount. New manager `SalesHistoryManager` + DB queries `AuctionDatabase.fetchPlayerHistory` / `countPlayerHistory`. Indexed via the new `idx_sales_time` on `sold_at`.
- **Short `/ah` alias** — Opens the AH dashboard directly without typing the namespace. Equivalent to `/arcadia_ah` (the root open command). Both go through the same `checkEnabled` gate.
- **Idempotent 1.3.0 schema migration** — `AuctionDatabase.migrateSchema_1_3_0` adds `featured` / `featured_until` to legacy `arcadia_prestige_auction_listings` rows in place via `INFORMATION_SCHEMA.COLUMNS` checks. Runs at `ServerStartedEvent`. No-op in singleplayer / debug mode. Read path tolerates missing columns via SQL exception swallowing so a half-migrated DB still serves correctly.

### Fixed

- **Race condition in `featureListing` would charge the fee on a listing that no longer existed** — Found during the 1.3.0 audit pass. The pre-fix path checked the in-memory cache, deducted the fee, and sent the success message — all synchronously, with no DB confirmation. If the listing was bought / expired between the cache hit and the deduct, the player paid for nothing and got a "you are now featured" confirmation. Fixed by moving the DB existence-check and promote into an async block AFTER the deduct, with explicit refund + a new `arcadia_ah.msg.feature_race_refunded` chat line and success message only sent inside the success branch on the server thread.
- **`§`-code injection in player-set broadcast templates** — Found during the 1.3.0 audit pass. The initial `PlayerSettingsManager.sanitize` kept `§` characters intentionally, which would have let any player inject obfuscated / coloured / bold text into every other player's chat through the broadcast path. Sanitiser now strips `§` AND the next character (the formatting code it would have consumed). The `{item}` / `{price}` placeholders are still resolved as Components and keep their legitimate upstream formatting.
- **Parallel `CopyOnWriteArrayList` misalignment in debug-mode sales log** — Found during the 1.3.0 audit pass. Two parallel lists (`DEBUG_SALES_META` and `DEBUG_SALES_AMOUNTS`) were independently appended in `logSale`; concurrent calls could interleave the two `add()` operations and produce mismatched buyer/amount pairs in singleplayer history + leaderboard. Replaced with a single `List<DebugSale>` where each row is immutable and atomic.

### Performance

- **Featured-first sort happens once per filter call, in-place** — `getFiltered` collects matches into a single `ArrayList` and performs one composite sort instead of post-filter re-iteration. Featured rows whose `featuredUntil` already passed sort with the unfeatured cohort (no need for a periodic sweep to unfeature them).
- **Per-player template cache** — `PlayerSettingsManager` caches each player's broadcast template on first read so the listing hot path never hits the DB. `Optional<String>` is used as the sentinel so "known unset" stays distinct from "never queried" and prevents duplicate fetches when several listings fire from the same seller in a row.
- **New `idx_sales_time` on `sold_at`** — Speeds up the windowed `/history` queries and the existing 30-day leaderboard query without changing semantics.

### Changed

- **`AuctionDatabase.insertListing` switched to explicit column-list INSERT** — Was a positional `VALUES (?,?,?,?,?,?,?,?,?,?,?)` that broke immediately when the new `featured` / `featured_until` columns were added. Now lists every column by name, so future migrations are non-breaking for existing call sites.
- **`AuctionDatabase.fromResultSet` tolerates missing `featured` / `featured_until`** — Wraps each new-column read in a try/catch so a half-migrated DB (legacy rows present, migration not yet run) still produces valid `AuctionListing` instances with `featured = false`.

### Ajouts

- **Sécurité shulker cross-serveur (validation NBT aller-retour)** — Le serveur valide qu'un objet soumis à une annonce survit à une sérialisation → désérialisation → diff champ-par-champ avant que la ligne soit écrite. Le composant `DataComponents.CONTAINER` (shulkers, bundles, conteneurs moddés) est comparé par nombre de slots non-vides, ce qui attrape le bug historique où un shulker acheté sur un autre serveur arrivait vide parce que le registre du buyer n'arrivait pas à résoudre certains items contenus. Les annonces refusées ne coûtent rien — le vendeur garde son objet ET ses frais. Implémenté via `AuctionItemSerializer.validateRoundTrip(...)`, contrôlé par le nouveau flag `config/integrity.strict_nbt_validation` (défaut `true`).
- **Mise en avant payante des annonces (featured)** — Les vendeurs paient un surcoût immédiat (`featured_fee_percent`, défaut 30 % du prix) pour mettre une annonce en avant pendant `featured_duration_hours` heures (défaut 24, plafonné à l'expiration naturelle). Les annonces featured remontent en tête de `getFiltered`. Deux points d'entrée : `/arcadia_ah sell <prix> [qté] featured` met en vente avec mise en avant directement, et `/arcadia_ah feature <listingId>` upgrade une annonce existante. Promotion race-safe : remboursement automatique + ligne chat explicite si l'annonce a été achetée / a expiré entre le clic et l'écriture DB. Nouveaux champs `AuctionListing` (`featured`, `featuredUntil`) persistés en MySQL (colonnes `featured`/`featured_until`) et SavedData solo.
- **Annonces globales dans le chat à chaque mise en vente** — Quand `config/broadcast.enabled = true` (défaut), chaque mise en vente déclenche une ligne globale `§6[HDV] <vendeur> met en vente <item> pour <prix> !`. Les joueurs personnalisent le texte via `/arcadia_ah sellmessage set <template>` avec les placeholders `{seller}`, `{item}`, `{price}`. Templates filtrés : caractères de contrôle ET codes vanilla `§` strippés avant broadcast pour empêcher l'injection chat — les placeholders `{item}` / `{price}` portent toujours leur propre formatage `Component`. Anti-spam : cooldown par vendeur (`per_seller_cooldown_seconds`, défaut 30s). Nouvelle table `arcadia_ah_player_settings(player_uuid PRIMARY KEY, sell_template, updated_at)`. Templates cachés en mémoire par UUID et évincés au logout.
- **Historique personnel des ventes (`/arcadia_ah history [page]`)** — Lit la table existante `arcadia_prestige_auction_sales_log`, paginé (`config/history.page_size`, défaut 10/page ; `config/history.max_days` fenêtre, défaut 30). Chaque ligne : date, nom de l'acheteur (résolu via lookup en ligne → profile cache → UUID court), type d'item, montant. Nouveau manager `SalesHistoryManager` + queries `AuctionDatabase.fetchPlayerHistory` / `countPlayerHistory`. Indexé via le nouveau `idx_sales_time` sur `sold_at`.
- **Alias court `/ah`** — Ouvre directement le dashboard HDV sans taper le namespace. Équivalent à `/arcadia_ah` (commande racine). Les deux passent par le même garde `checkEnabled`.
- **Migration de schéma idempotente 1.3.0** — `AuctionDatabase.migrateSchema_1_3_0` ajoute `featured` / `featured_until` aux lignes legacy de `arcadia_prestige_auction_listings` en place via vérifications `INFORMATION_SCHEMA.COLUMNS`. Tourne au `ServerStartedEvent`. No-op en solo / debug. Le chemin de lecture tolère les colonnes absentes via swallowing SQLException pour qu'une base à moitié migrée serve quand même.

### Correctifs

- **Race condition dans `featureListing` chargeait les frais sur une annonce inexistante** — Trouvée pendant l'audit 1.3.0. L'ancien chemin lisait le cache mémoire, déduisait les frais et envoyait le message de succès — tout en synchrone, sans confirmation DB. Si l'annonce était achetée / expirait entre le hit cache et la déduction, le joueur payait pour rien et recevait quand même un "your listing is now featured". Corrigé en déplaçant le check d'existence DB et le promote dans un bloc async APRÈS la déduction, avec remboursement explicite + nouvelle ligne chat `arcadia_ah.msg.feature_race_refunded`, et le message de succès n'est envoyé qu'à l'intérieur de la branche succès sur le thread serveur.
- **Injection de codes `§` dans les templates de broadcast joueur** — Trouvée pendant l'audit 1.3.0. Le `PlayerSettingsManager.sanitize` initial conservait les caractères `§` intentionnellement, ce qui aurait permis à n'importe quel joueur d'injecter du texte obfusqué / coloré / gras dans le chat de tous les autres via le broadcast. Le sanitizer strippe maintenant `§` ET le caractère suivant (le code de formatage qu'il aurait consommé). Les placeholders `{item}` / `{price}` restent résolus en Components et conservent leur formatage upstream légitime.
- **Désalignement de `CopyOnWriteArrayList` parallèles dans le sales log debug** — Trouvée pendant l'audit 1.3.0. Deux listes parallèles (`DEBUG_SALES_META` et `DEBUG_SALES_AMOUNTS`) étaient appendées indépendamment dans `logSale` ; des appels concurrents pouvaient entrelacer les deux `add()` et produire des paires acheteur/montant désappariées dans l'historique + leaderboard solo. Remplacé par une seule `List<DebugSale>` où chaque ligne est immuable et atomique.

### Performance

- **Tri featured-first en une passe** — `getFiltered` collecte les matches dans une seule `ArrayList` et fait un tri composite unique au lieu d'une ré-itération post-filtre. Les lignes featured dont la fenêtre est expirée trient avec les non-featured (pas besoin de sweep périodique pour les défeaturer).
- **Cache par joueur des templates** — `PlayerSettingsManager` cache le template de broadcast au premier accès, donc le hot path de mise en vente ne touche jamais la DB. `Optional<String>` sert de sentinelle pour distinguer "connu vide" de "jamais demandé" et éviter les fetchs en double quand un même vendeur enchaîne plusieurs ventes.
- **Nouvel index `idx_sales_time` sur `sold_at`** — Accélère les requêtes fenêtrées de `/history` et la requête leaderboard 30j existante sans changement sémantique.

### Modifications

- **`AuctionDatabase.insertListing` passé à un INSERT avec liste de colonnes explicite** — C'était un `VALUES (?,?,?,?,?,?,?,?,?,?,?)` positionnel qui cassait dès qu'on ajoutait `featured` / `featured_until`. Liste maintenant chaque colonne par nom, donc les futures migrations seront non-cassantes pour les call sites existants.
- **`AuctionDatabase.fromResultSet` tolère les colonnes `featured` / `featured_until` manquantes** — Wrappe chaque lecture des nouvelles colonnes dans un try/catch pour qu'une base à moitié migrée (lignes legacy présentes, migration pas encore tournée) produise quand même des `AuctionListing` valides avec `featured = false`.