Medici Download v1.2.1
The ledger · docs

For modpack makers

Data formats, KubeJS events, turning sources off.

Coins are data

Which items are money, and what they're worth, is the medici:coin_values data map: data/medici/data_maps/item/coin_values.json in your datapack (a data map's files always sit in the map's own namespace, medici, whatever namespace your pack uses; NeoForge merges the files of every pack). Medici's own:

{
  "values": {
    "medici:penny": { "value": 1, "symbol": "P" },
    "medici:groat": { "value": 10, "symbol": "G" },
    "medici:ducat": { "value": 100, "symbol": "D" }
  }
}

Add a coin (any item, any value in pennies, a short symbol for prices) or change values in a datapack. Every Medici block reads this map; change-making works with any set (a 25-penny coin is fine). Tags: #medici:coins, #c:coins, #medici:coins/copper|silver|gold.

Turning sources down

All new money passes the decree (medici-server.toml, CONFIG). Fewer coins on your server: lower targetPerPlayer. No mob coins: mobCoinsPerDay = 0.

Found money

  • Mob drops: data/<ns>/medici/mob_drops/*.json, first matching rule wins; files are read in order of their path, then their namespace (a.json before b.json, whatever the namespace): {"entities": ["minecraft:pillager", "#c:bosses"], "chance": 0.6, "min": 1, "max": 3, "rare_value": 10, "rare_chance": 0.08, "antiques": 0, "antique_chance": 0} (pennies). Only real player kills of mobs that spawned naturally, from world generation, structures, patrols or raids pay; spawners, eggs, breeding and machines never do. Daily cap per player: mobCoinsPerDay.
  • Chest loot: the global loot modifier medici:coins_in_chests (data/medici/loot_modifiers/coins_in_chests.json) holds rules {"tables": ["minecraft:chests/village/*", "yourmod:*"], "chance", "min", "max", "antiques", "antique_chance", "letter_chance"}. Override the file in a datapack to add your structures.
  • Antique coins: the medici:antique_values data map (item → pennies paid at a bank counter), in data/medici/data_maps/item/antique_values.json.
  • Appraisal: data/<ns>/medici/appraisal/*.json, {"values": {"minecraft:bread": 0.5, "#c:ingots/iron": 0.6}} (pennies per item; files in order of their path, then namespace, later entries override earlier ones). The Merchant wishes for items priced 0.01 to 40 p: every item listed by id, plus every item in the medici:merchant_wishes item tag that the appraisal prices at all (by id or through a tag entry). Tagged items come up 4 times as often. Add your mod's legal goods to both: a price here, an entry in data/medici/tags/item/merchant_wishes.json.

KubeJS

With KubeJS (2101.7+, as in ATM10 8.4) Medici adds three things to server scripts. Examples you can copy are in examples/kubejs; CI loads every one of them on a test server.

Events: MediciEvents.<name>(event => …). They fire before anything moves; event.cancel() refuses the action (the player sees "Not allowed here") and nothing changes.

event when properties cancel
purchase a player pays at a shop, coin slot, turnstile or rental chest, or sells to a shop player, pos, kind (sale, buy, coin_slot, turnstile, rental), item, price (pennies), ownerHouse yes
mint a mint press or die press is about to strike level, pos, house, result, value, source (mint, die_press) yes
transfer a bank transfer player, from, to, amount, fee yes
ticket a valid ticket at a ticket gate player, pos, venue, venueName, kind yes
commission the Artist is about to take a paid commission patron, commission (kind, tier, size, …), price (settable, pennies) yes
merchantWishes the Merchant arrives with his wishes level, tradingHouse, wishes (a list you can change), add(item, pennies), clear() no

Event strings are Java strings: compare with String(event.kind) === 'sale'.

MediciDecree: supply(server) (coins in the world, pennies), houseLeft(player) (what their house may still mint today), wallet(player) (coins they carry), format(pennies) (1D 4G 5P), and charge(player, pennies, reason): takes the coins (purses first, change given) and burns them through the mint ledger; false if they can't pay. Scripts can burn but never mint: new coins only come from the decree.

Recipe schema: event.recipes.medici.minting(result, metal, die) with optional .energy(fe) and .time(ticks).

Other mods can use the same events in Java: de.benkl.medici.api.MediciEvent.* on NeoForge.EVENT_BUS.

example what it does
medici.js a richer groat recipe, no dirt trade, a stamp duty on big transfers, Merchant wishes, a ride counter
claim_charter.js /charter: one extra FTB Chunks claim for 2 ducats, burned
waystone_toll.js opening a waystone costs 5 p
quest_shop.js /shop <item>: quest helpers for coins
exchange.js one-way exchange into Lightman's Currency or Numismatics coins, if the pack has them

More (KubeJS events, recipe schemas) as the features land (#52).

Java API: MediciTreasury

For mods that pay or charge players money (traders, contracts, quest boards): de.benkl.medici.api.MediciTreasury. It keeps Medici's rule that new coins only come from the decree (ECONOMY §2): your mod registers a purse, a Medici bank account (trader:<purse id>) that takes in the coins your traders earn and pays out from them first, then from the decree's street share. Registering a purse turns that share on; a server without your mod keeps the old split.

Compile against the API jar from the release (medici-<version>-api.jar, only de.benkl.medici.api, with sources) and guard every call with ModList.get().isLoaded("medici"):

dependencies { compileOnly files("libs/medici-1.21.1-1.1.0-api.jar") }
// during setup (any thread)
MediciTreasury.registerPurse("mymod:street");

// on the server thread
long paid = MediciTreasury.pay(player, "mymod:street", 250, "mymod:contract");   // 0..250, purse first, then the street
if (MediciTreasury.charge(player, "mymod:street", 40, "mymod:shop")) { /* hand over the goods */ }
Component price = Component.literal(MediciTreasury.format(40));                   // "4G"
method what
registerPurse(id), isRegistered(id) register your purse during setup (idempotent); turns the street share on
available(server, purse) the purse's balance plus today's street pool left (0 if not registered)
withdraw(server, purse, pennies, reason) up to pennies for a payout, purse first, then new coins from the street pool (minted, in the ledger under reason); you hand out exactly the granted amount
deposit(server, purse, pennies, reason) coins your trader took in (you removed the items) go into the purse
burn(server, pennies, reason) a sink of yours destroyed coins (you removed the items); a ledger burn
pay(player, purse, pennies, reason) withdraw and give the coins (purses first, then the inventory, dropped if full); returns what was paid
charge(player, purse, pennies, reason) take the coins (purses first, change given back) into the purse; false and nothing moves if they can't pay
coins(pennies), value(stack), wallet(player) coin stacks worth an amount (largest first), what a stack is worth, what a player carries
appraisal(stack), format(pennies) the Merchant's price for one item (the appraisal table), 1D 4G 5P

Amounts are pennies. Everything but registerPurse/isRegistered runs on the server thread only. A purse id that isn't registered makes withdraw, deposit, pay and charge throw IllegalArgumentException. At each decree day start a purse keeps at most max(1,000 p, 3 × the street pool); the rest is burned, so pay out what you take in.

Add your own art

Everything the Artist paints is data (concept §4.9.3), read on /reload; a bad file is logged and skipped. Ids are <namespace>:<path>; names come from lang keys art.<ns>.<path with dots> (motifs), frame.<ns>.<id>, pose.<ns>.<id>, theme.<ns>.<id>.

  • Motifs data/<ns>/medici/art/motif/**/*.json:
    { "kind": "portrait", "theme": "medici:canal", "rarity": "uncommon", "set": "my_pack",
      "texture": "mypack:art/background/harbour", "masterwork_texture": "mypack:art/background/harbour_night" }
    
    kind: portrait (textures textures/<texture>_<w>x<h>.png for 1x1, 1x2, 2x2, 3x2 at 16 px per block), city_view ("painting": "mypack:harbour" = a painting_variant you also add, with "sizes": ["3x2"]; "dimension" hides it until that dimension exists), fresco (textures/<texture>_<w>x<h>.png on the block atlas, i.e. under textures/block/, sizes 2x2, 3x3, 4x3, 6x4), mask (texture namespace + pattern id: textures/models/armor/mask/<shape>_<pattern>.png and item/mask/<shape>_<pattern>.png), crest ("pattern": a banner_pattern), statue ("ramp": ["#rrggbb", …] dark → light), colossus ("scheme": {"shades": […], "core", "ornament", "eyes", "plinth", "plinth_trim"}, block ids). rarity (common 6 : uncommon 3 : rare 1 weight) decides the odds: Sketch draws common, Study common and uncommon, Commission and up everything. Optional "sizes" limits a motif to some sizes.
  • Frames data/<ns>/medici/art/frame/*.json: {"family": "wood", "texture": "mypack:art/frame/oak", "shape": "rect"}; the texture is a 16×16 nine-slice with a 3 px border; families plain (Sketch), wood (Study), gilt (Grand, or Commission with gold leaf), anything else from Commission tier; shapes rect, oval, round.
  • Poses data/<ns>/medici/art/pose/*.json: {"kinds": ["portrait", "statue"], "bust_only": false}; the built-in drawing knows standing, hand_on_hip, holding_ducat, ledger_and_quill, gondolier_oar, masked, pointing, seated, orator, saluting (other ids draw as standing).
  • Themes data/<ns>/medici/art/theme/*.json: {"order": 20}.

Missing textures show a plain canvas (portraits) or the missing-texture sprite (frescos), never a crash. To change prices or pools in code, listen to de.benkl.medici.arts.CommissionEvent (see INTEGRATIONS).