Cost
The abstraction for what an ability, a formation or another action consumes. A Cost does one thing: given the context of a payment it prices itself into a bound payment transaction (Transaction) - one fixed store at one fixed amount. It never asks a store whether it can pay, never writes, and remembers nothing between calls, so "what this costs" is decided in exactly one place while "can it be paid" and "take it" belong to the payment.
| Member | Description |
|---|---|
Result<Transaction> prepare(CostContext context) | Prices this entry for one payment: the amount is evaluated once, the store is resolved from the context and the parameters are frozen. No stock check, no script call, no chunk loaded, no attachment created. An amount that cannot be priced (a formula that is not a finite positive number) is refused here, so a payment that could only be priced in part charges nothing. |
MapCodec<? extends Cost> codec() | The codec of this concrete cost type. |
Codec<Cost> TYPED_CODEC | Dispatched on the type field. |
Codec<Cost> CODEC | The one data files use: it tries a typed object first and then the { "id": ..., "amount": ... } shorthand, read as saturn:resource. |
Codec<List<Cost>> LIST_CODEC | The list codec used by every field that takes several costs at once. |
Transaction is one bound fee, Cost.Transaction, and it has exactly four members:
| Member | Description |
|---|---|
CostTarget target() | The store this fee comes out of, and what kind of channel it is. Two fees of one payment naming the same target are the same store. |
double amount() | The fixed amount, in that store's own unit: a real number, whole items, fractional bank aura, one script call. |
Result<List<Debit>> execute(boolean simulate) | Both passes go through this one call: true only asks and writes nothing; false takes from its channel and answers with what it really took. The first real answer is terminal, so one fee is never taken twice. A refusal carries no debits. |
Transaction withAmount(double amount) | The same binding at another fixed amount, which is how a payment re-prices a fee without re-running the formula that priced it. Zero is dropped by the payment rather than executed as an empty fee. |
Four built-in types are registered in saturn:cost_type: saturn:resource, mxt:aura, mxt:item and mxt:js. Adding one means writing an implementation and registering it in MxtRegistries.COST_TYPE; the field shape does not change, since every cost field is this array - see Shared Data Types.
The payment CostPayment is the only entry point: price only prices, resourceAmounts() / auras() / resources() are read-only projections, replaceResourceAmounts and scale edit the bill, test simulates one pass and pay simulates every fee and then takes them all. An edit changes the bill itself - a replaced resource fee is a new fixed transaction, a scaled amount is a new fixed transaction - so the simulated and the real pass work on one set of amounts and nothing is evaluated a second time after the price was read.
Repeated targets are not a load error. Before execution, LoggerDispatcher logs WARN in development mode and DEBUG in production. Entries are not merged; both passes retain List order, including script positions. The same script id for the same payer is diagnosed separately while calls remain independent. Prefer one entry for a target's whole fee.
A simulation failure prevents all real deductions. Once all simulations pass, a real failure or RuntimeException only calls plain LOGGER.warn, without dispatch, propagation, rollback or cancellation. Later fees and business effects continue; the result is successful and contains known actual Debits. Success does not promise full collection. Partial receipts from failed fees are retained, unknown amounts are not invented, and the cached result prevents another pay from writing again.
Why the shape is drawn this way, and what "one target written twice" used to go wrong with, are on Cost.
The payer and the channels come from the call site (CostContext carries the payer and which channels it offers), and a datapack only writes what is wanted: mxt:item and mxt:js need a player, and a missing channel is a refusal rather than an error; a field such as a formation's upkeep names a resource account directly, because its owner may be offline. Currency is not a cost: the prices and the value multiplier in currency never become a Cost. The aura entries of a talisman are the one exception in this shape - they spend the charge the carrier itself was filled with, and its capacity comes from a separate multiplier field.
AuraCostSource.bind(CostContext, Holder<Aura>, double) binds an aura source to a fixed transaction. CostContext carries payer, formula, origin, account, auraSource and itemTarget; use of / account / pool / bank or extend it with withAuraSource. Binding resolves the target without checking or changing stock; execute(simulate) handles checks and deductions. Account aura reuses the resource channel, while world pools and banks share AuraAccess transfers. Bank amounts and receipts are doubles without upward rounding. Java integrations must recompile against the new signatures.
origin is a String used only for diagnostic logging. It does not determine prices, permissions or payment channels. com.iafenvoy.mxt.data.cost.context.CostSources provides shared string constants for built-in labels; extensions can pass their own labels without registration.
CostContext.of(payer, CostSources.ABILITY);
CostContext.of(payer, "example:ritual");