Trading API
Open authoritative merchant catalogues and query personal trade rapport inside an active dialogue.
TradingAPI opens a merchant catalogue in an active Blabber conversation and exposes personal rapport ranks. Villagers and NPCs share catalogue generation, payment, stock ledgers and courtesies. Player rules are in Trading.
Entry point
Use net.kzeroko.isekaiexpansion.api.trading.TradingAPI, which delegates to the internal DialogueTrading service. The actions blabber:open_trade and isekaiexpansion:open_npc_trade reach the same service. Blabber owns panel navigation and single-use reply tokens; the server owns all mutations.
Common tasks
Open the current speaker’s shop
Blabber.getActiveDialogueEntity(player)
.ifPresent(merchant -> TradingAPI.open(player, merchant));
Import io.github.ladysnake.blabber.Blabber and the facade. The entity must be a MobEntity implementing Merchant, or a BaseNpcEntity. Admission requires an active dialogue targeting that exact entity. Rejected admission returns false.
Bind an NPC profession
Set trade_profession in the NPC preset to a catalogue ID such as minecraft:farmer. Author the matching catalogue under data/<namespace>/trade_overrides/; Trade overrides describes schema version 3. A custom job needs no villager profession registration. Gate optional provider mods with requires_mods.
BaseNpcEntity.getTradeProfession() resolves a common NPC’s saved assignment before its preset. setTradeProfession(identifier) assigns a common NPC job; null returns to the preset. Unique NPCs reject instance assignment and always use their preset. Evelina’s preset selects isekaiexpansion:mahoutsukai_evelina. canTradeWith requires an enabled catalogue and the preset’s trade_access: trusted by default, or public. Both the disabled menu choice and server admission use this predicate. Unique NPCs exclude courtesy, refresh and bargaining actions on both rendering and execution paths; automatic stock remains active.
These entity methods are internal integration points. The preset field is optional and defaults to no trade profession; malformed identifiers reject that preset. TradeProfession is the corresponding optional entity NBT string. The humanoid export command preserves the effective profession.
Query recognition
int rank = TradingAPI.rapportRank(player, merchant);
merchant is a MobEntity. The result belongs to this player–merchant pair. The villager dialogue handler uses rank 3 for familiar scenes, resolving profession paths from Registries.VILLAGER_PROFESSION.
Reference
| Entry point | Side and contract |
|---|---|
TradingAPI.open(player, merchant) | Server thread and active entity-bound dialogue. Returns admission success and opens the blocking catalogue. |
TradingAPI.rapportRank(player, merchant) | Server thread; returns rank 1–5, creating a zero-progress record on first lookup. |
DialogueAPI.registerTradePanel(opener) | Blabber setup hook; the host handles villagers and NPCs through the same service. |
TradeOverrideManager generates every tier 1–5 immediately, with deterministic merchant UUID, profession, tier, offer-ID and rotation seeds. VillagerTradeAuthorityMixin disables vanilla level advancement; unowned professions invoke their vanilla/mod factories once for each tier. Owned lists are frozen at the construction and getter boundaries. Rebuilding preserves compatible offer state and the profession/offer-ID use ledger. The cache depends on definition, revision and rotation, never rapport.
TradeTransaction prices canonical currency costs using TradePricing before bargaining, validates both inputs, currency and output capacity, then pays using Isekai Core NumismaticCompat. The displayed exchange uses that same quote. Currency change can occupy space during payment; remaining output uses offerOrDrop. Villager Merchant.trade runs with a customer scoped to its synchronous callback and cleared in finally. Villager experience no longer advances catalogue access. NPCs use their shared authoritative list directly.
After payment, the service records stock use and personal rapport. MerchantCourtesies revalidates the definition, session, rank, charge and item before giving or repairing anything. Maintenance compares the selected main-hand snapshot again and changes damage only. Gift capacity is checked before charge consumption.
Register a courtesy action
net.kzeroko.isekaiexpansion.api.trading.CourtesyAPI.register(type, parser) delegates to the internal registry. Register during common initialization, before resource reload. Duplicate IDs throw. The parser receives a copied JsonObject including type and returns an immutable CourtesyAction implementation from the internal package, exposed only for this extension contract. Reject unknown or malformed fields in the parser.
icon(player) and description() supply presentation; canApply(player, merchant) is a side-effect-free eligibility check. apply(player, merchant) must revalidate, run synchronously on the server thread and return false without mutation if it cannot complete. The dispatcher spends a courtesy only after true. Never retain the player or world in parsed action instances. Built-ins implement gift, repair, meal, cleanse and timed effect actions. Payload fields are in Trade overrides.
Reputation quotes and information
TradePricing.resolve reads identity and location factions and current villager gossip. Gossip is normalized by 4 and clamped to −1000…1000; the worst negative source wins, otherwise the best positive source wins. Hero of the Village supplies a positive floor of 100 per amplifier step, capped at 500, only when standing is nonnegative. This avoids adding the faction-to-gossip bridge twice. Purchase modifiers span −15%…+40%; payout modifiers span −20%…+5%. Whole percentage points are applied with overflow-safe integer arithmetic, then purchase-only bargaining. Currency is read from original stacks; noncurrency inputs retain vanilla adjustments. Each catalogue resolves one pricing context, and confirmation resolves again before comparing the complete quote.
MerchantDialogueMetadata sends rank, score, next threshold, courtesy count/capacity and purchase/payout modifiers. MerchantDialogueHud uses DialogueInfoBar, the same compact strip as the NPC status HUD. Both reserve their own top row through Blabber; narrow text is shortened with an ellipsis. Blabber object updates refresh metadata. DialogueAPI.lastSpeech(player) retains the last authored line across an empty ending action; the shop preserves unique speech rather than adding a generic greeting.
Persistent entity fields
IsekaiCommerce is lazy internal save data. These fields describe the current format; they are not a data-pack schema.
| Field | Type | Default | Behaviour |
|---|---|---|---|
Rotation | long | 0 | Nonnegative selection rotation. |
Refresh | boolean | false | One stored refresh opportunity. |
Previous | list<string> | [] | Prior selected IDs, used to prefer unseen candidates. |
NpcDay | long | Minimum long | Last elapsed overworld day. Advancing it clears stock and negotiations and grants refresh; first initialization grants none. |
Negotiations | compound | Empty | Player UUID to discount 0, 5, 10 or 15; absent means no attempt. |
Stock | compound | Empty | Profession/offer-ID to used count; actual restock clears it, rotation preserves it. |
Rapport | compound | Empty | Player UUID to the following relationship state. Invalid UUID entries are ignored. |
Rapport.<uuid>.Score | integer | 0 | Clamped to 0–360. Thresholds are 0, 24, 80, 180 and 360. |
Rapport.<uuid>.Courtesies | integer | 0 | Clamped to current capacity: 1, or 2 from rank 4. |
Rapport.<uuid>.Day | long | Minimum long | Last progress day; only forward changes reset daily limits. |
Rapport.<uuid>.DailyProgress | integer | 0 | Clamped to 0–24. |
Rapport.<uuid>.CourtesyProgress | integer | 0 | Clamped to 0–40 credited progress towards a recurring courtesy. |
Rapport.<uuid>.CourtesySpend | long | 0 | Clamped to 0–1000 net currency spending towards that courtesy. |
Rapport.<uuid>.Repetitions | compound | Empty | Offer-key counts 0–3; reads at most 24 entries. |
IsekaiOfferId | string | Empty | Separate per-offer NBT field for stable authoritative identity. |
Threading and lifecycle
Call gameplay methods on the server thread; opening off-thread throws. Each merchant admits one trading player at a time. Admission checks life, world, 4-block range, combat, panic, current merchant customer and the NPC catalogue policy. Sessions capture dialogue UUID, entity, offer list, rotation, day, profession and catalogue revision. A changed profession, revision or NPC day invalidates a displayed action. Reload, disconnect and server stop clear active sessions.
Confirmation checks the live offer object and current quote before payment. Blabber consumes its reply token before dispatch. Lifecycle dismissals do not reopen panels. Villager restock and forward NPC day transitions clear bargaining and stock uses; refresh only rotates selection. Rapport persists through both. The day bucket uses overworld elapsed time divided by 24000 ticks (1200 seconds at 20 ticks per second).
Verifying
Run ./gradlew.bat -I tools/trading/verify.gradle checkCommerce processResources and python tools/trading/validate_coverage.py. The JVM checks exercise production parsing, rapport limits, payout budgets, save/reload and stock transitions. Coverage checks compare the configured profession dump with catalogue, scene and bilingual-text resources.
In game, check a fresh level 1 villager, two players with different rapport, all 5 catalogue tiers, repeated trades, restart, restock and rotation. Test full-inventory gifts, changed held-item repair, changed NPC profession and a reload while a quote is open. Open a fresh cartographer’s service menu with and without goods. REI shows bundled catalogue tiers and first-visit availability. These interactive checks require a running pack.
Related
- Trading — player rules.
- Trade overrides — catalogue and courtesy fields.
- NPC dialogue — admission and routing.