Skip to content

Development

How to work on MineDnD and how it's built: setup, architecture, data flow, hooks, networking, build and tests. What the mod does for players is in How MineDnD works; releasing is in Releasing. Every class also has a comment with a Functional and a Technical part.

Getting set up

You need: a JDK 25 or newer (e.g. Amazon Corretto, Temurin) and IntelliJ IDEA (Community is fine). Gradle doesn't need to be installed: the project uses the wrapper (gradlew). The docs tools need Node.js (the content docs) and Python 3.9+ (the site).

  1. Open the project folder in IntelliJ and let it import the Gradle project (the first import downloads Minecraft, a few minutes).
  2. In File → Project Structure → SDK, choose your JDK 25+.

If Gradle says JAVA_HOME is not set, point it to your JDK, e.g. in PowerShell: $env:JAVA_HOME = "$env:USERPROFILE\.jdks\corretto-26.0.2.1".

Tips: - Minecraft 26.3 is not obfuscated: class and method names are Mojang's, so tutorials for older versions often use different names. - New files created outside IntelliJ show "Cannot resolve symbol": use File → Reload All from Disk or reload the Gradle project. - If a mixin targets a method that doesn't exist, the game crashes at startup with a mixin error that names it. - Gameplay numbers (all the stat formulas) are in gameplay/AbilityEffects, with their defaults in settings/Settings.

Build, run and test

From a terminal in the project folder (on Windows .\gradlew, on macOS/Linux ./gradlew):

Command What it does
gradlew build Compiles the mod → build/libs/MineDnD-<version>.jar (+ sources)
gradlew runClient / runServer Starts Minecraft (or a server) with the mod; also from IntelliJ: Gradle panel → Tasks → fabric → runClient
gradlew runClientWithContent runClient with the Default Content: its datapack is on in every world (a built-in pack of the test mod, listed as "MineDnD Default Content (tests)"; edits are picked up at the next launch) and its resource pack is copied into run/resourcepacks and turned on the first time (F3+T after gradlew syncDevResourcePack reloads it)
gradlew runClientGameTest In-game tests: starts Minecraft, plays the scenarios in src/gametest in a new world and saves screenshots in build/run/clientGameTest/screenshots
gradlew buildMrpack build/distributions/MineDnD-<version>.mrpack: Fabric API and the modpack/ extras as Modrinth downloads, the mod jar and configs as overrides
gradlew installToModrinth -Pinstance="<folder>" Builds the mod and swaps it into a Modrinth App instance (without -Pinstance it lists them; -PmodrinthProfiles=<dir> looks elsewhere)
gradlew buildDatapack build/distributions/MineDnD-Default-Content-<version>.zip: the Default Content datapack
gradlew buildResourcePack build/distributions/MineDnD-Default-Content-Resources-<version>.zip: its resource pack (also in the .mrpack's resourcepacks/, without the version in the name)
gradlew runDocs The documentation site on http://127.0.0.1:8000/ with live reload, until Ctrl+C (-Pport=8001 for another port); see Documentation site
gradlew buildDocs Builds the documentation site into site/ the strict way CI does: a broken link or anchor fails it
gradlew runPackEditor Starts the MineDnD Pack Editor app, reloading as you edit; see MineDnD Pack Editor

The version lives only in gradle.properties (mod_version). It flows into the jar name, fabric.mod.json and the modpack. When updating Fabric API, also update modrinth_fabric_api_version_id there (see the comment).

Trying it in game (runClientWithContent; plain runClient has no content, as the mod jar has none): 1. Singleplayer → Create New World → MineDnD example world (Game tab): Survival on Hard with commands allowed, and chests in front of you with every MineDnD item and enchanted book. Or create a world with Allow Commands: ON and run /dnd catalog chests. 2. Try: K (character sheet), /dnd stats set @s str 20, /dnd level add @s 2, /dnd catalog list, the MineDnD creative tab.

Dev worlds live in run/saves/ (ignored by Git).

Stack

Minecraft 26.3. Not obfuscated, so the code uses Mojang's names (Identifier, GuiGraphicsExtractor, minecraft.gui.setScreen, …)
Loader Fabric Loader 0.19.5, Fabric API 0.162.0+26.3, Loom 1.18
Language Java 25 (any JDK 25+ builds it)
Mixins SpongePowered Mixin plus MixinExtras (@WrapOperation, @ModifyExpressionValue, @Local)
Tools GitHub Actions for CI and releases; Node.js scripts in tools/ for the content docs; Tauri 2 (Rust + TypeScript, Vite, CodeMirror) for the Pack Editor app; MkDocs Material for the docs site

Repository layout

Path Contents
src/main/java/org/lesshub/ Common code (server and client): see the packages below
src/client/java/org/lesshub/client/ Client only: entrypoint, character sheet, brewing recipe list, translations, example world button, client mixins
src/gametest/ In-game tests (Fabric client game tests): gradlew runClientGameTest
src/main/resources/ fabric.mod.json, mixin configs, assets/minednd (icon, mod texts in en/it)
src/main/resources/minednd/default_settings.json Every world setting at its default: a reference file read by /dnd info settings, not a datapack. Kept by hand; InfoClientGameTest checks it equals Settings.DEFAULT
datapacks/default-content/ The MineDnD Default Content datapack (items, recipes, enchantments, tags, rules, achievements, titles, translations), released as a zip; the mod jar has no content
resourcepacks/default-content/ The Default Content's resource pack (item icons by IvoryRed, CC BY 4.0; credits in its CREDITS.md), released as MineDnD-Default-Content-Resources-<version>.zip and in the .mrpack
datapacks/example/ The pack of the docs' tutorial ("Your first datapack"), loaded by ExamplePackClientGameTest
datapacks/templates-test/ Content for testing limited and hidden items, smithing templates and loot by hand, with a checklist in its README; not shipped
tools/ generate-content-docs.js (the generated docs pages), generate-vanilla-names.js and its output data/vanilla-names.json (Minecraft's English and Italian names), docs-site.sh (local preview of the site), pack-editor/ (the MineDnD Pack Editor app, see below)
modpack/ Extras for the .mrpack (Modrinth files list + config overrides)
modrinth/ Modrinth listing texts
.github/ CI, Release, docs, game tests and Pack Editor downloads workflows, the Modrinth upload script, rulesets, PR template
docs/ Documentation: en/ and it/ (the Italian set covers the user-facing pages), see the index

Packages in src/main/java/org/lesshub/:

Package Responsibility
(root) minednd Entrypoint: registrations and server events
models Immutable data: PlayerStats, PlayerProgress, Ability, AbilityRequirements, StatBonus, ActiveAbility
gameplay Rules applied in game: AbilityEffects, EffectiveStats, ActiveAbilities and AbilityPowers (item abilities), AdventureXp, BlockBreakContext
advancement ModTriggers and the six achievement triggers (repeat, discovered, statistic, cooked, level, ability_score)
title Title, TitleEffect (effect types), Titles (loader, sync, unlocking, effect helpers)
settings Settings (record tree + codecs; LevelCurve for level costs), XpFormula (the level_xp_formula parser) and SettingsLoader
catalog MineDnD items: ItemCatalog (server), ClientCatalog, CatalogEntry; LimitedItems (copies made per world), HiddenItems (who is told about hidden items)
crafting Requirement rules (CraftingRequirements, ItemRules, RuleAction); the recipe types CatalogRecipe, CatalogBrewingRecipe, CatalogSmithingRecipe, CatalogCookingRecipes, CatalogFletchingRecipe, CatalogFletchingUpgradeRecipe, CatalogImbuingRecipe; the shared station check CraftingRequirements#mayMake (rules and limits); CatalogItemIngredient and BaseItemRule (MineDnD items aren't their base item); RecipeUnlocks; the brewing recipe list (BrewingGuide)
fletching The fletching table: FletchingTable (opening it, its tabs, menu types), FletchingMenu (the grid) and FletchingUpgradeMenu (templates), FletchingRecipes (recipe types, what the grid makes, rules and limits), FletchingGuide (the recipe list and the upgrade slots' filters)
enchanting The enchanting table's Imbue tab: Imbuing (recipe and menu types, switching tabs), ImbuingMenu, ImbuingGuide (the recipe list and the slot filters)
entity Ability projectiles: SpellBolt, ThrownWeapon
loot The minednd:ability_scores condition; the minednd:catalog_item entry (CatalogItemEntry) and loot injections (LootInjections, InjectedLootEntry)
i18n Translations from datapacks
network Payloads between server and client
registry Attachments, item components, entities, creative tab, loot condition and entry types
command /dnd commands
world Item chests and the example world
mixin Hooks into vanilla code

Architecture

flowchart LR
  subgraph Data["World datapacks (Default Content + others)"]
    items["items/"]
    recipes["recipe/"]
    rules["crafting_requirements/"]
    lang["lang/"]
    settings["settings.json"]
    ench["enchantment/ + tags"]
  end
  subgraph Server
    loaders["Reload listeners<br/>ItemCatalog · CraftingRequirements · DatapackTranslations · SettingsLoader"]
    state["Server state<br/>catalog · item rules · translations · Settings.server"]
    attach["Player attachments<br/>stats · progress · visited biomes · first times"]
    gameplay["Gameplay<br/>AbilityEffects · ActiveAbilities · AdventureXp · requirements"]
  end
  subgraph Client
    cstate["Client state<br/>ClientCatalog · ItemRules client · ClientTranslations · Settings.client"]
    ui["UI<br/>character sheet · tooltips · creative tab · keys"]
  end
  Data --> loaders --> state
  recipes --> vanilla["Vanilla RecipeManager<br/>(CatalogRecipe)"]
  ench --> vanilla2["Vanilla enchantment registry"]
  state -- "sync payloads (join, /reload)" --> cstate
  attach -- "attachment sync" --> ui
  cstate --> ui
  ui -- "SpendStatPoints · UseAbility" --> gameplay
  gameplay --> attach

Principles: - Data-driven. Items, recipes, enchantments, rules, translations and numbers come from datapacks (the Default Content datapack and other world packs) and are reloaded with /reload. Code holds mechanisms, not content. - Server-authoritative, client-predicted. The server decides and saves. Clients get copies (settings, rules, catalog, stats) so their prediction and UI match: mining speed, bow timing, place/use blocking, tooltips. Client requests (spending a point, using an ability) are re-checked on the server. - Two slots for shared state. In singleplayer the integrated server and the client run in one JVM and share classes, so Settings and ItemRules keep a server and a client copy. Settings.of(level) picks the right one. - Immutable values. Player data are records replaced as a whole. The Fabric attachment API saves and syncs only on setAttached, so every change goes through a copy.

Player data (attachments)

Attachment Type Persistent Kept on death Synced
minednd:player_stats PlayerStats (six base scores) yes yes owner only
minednd:player_progress PlayerProgress (level, unspent points, XP) yes yes owner only
minednd:visited_biomes Set<Identifier> (biomes) yes yes no
minednd:first_times Map<String, Set<Identifier>> (kind → ids: mob entity types, death damage types, craft items, …) yes yes no
minednd:criterion_counters Map<String, Integer> ("advancement/criterion" → count, for minednd:repeat) yes yes no
minednd:active_title Identifier (worn title; absent = none) yes yes everyone tracking the player
minednd:recipes_relocked Boolean (old all-recipes grant already undone) yes yes no
minednd:ability_boosts Map<Identifier, Long> (attribute → game time a buff boost ends) no no no
minednd:imbue AbilityPowers.Imbue (charged hits left, what they add, until when) no no no
minednd:leap Long (game time a leap started: no fall damage until landing) no no no
minednd:unlocked_titles Set<Identifier>, worked out from advancements by Titles.refresh no recomputed on respawn owner only
  • Defaults: new characters start at Settings.progression.startingScore. Fields added later use optionalFieldOf with a default (e.g. xp), so old saves keep loading.
  • Effective stats (EffectiveStats) = base + minednd:stat_bonus of equipped items per slot group. An item with minednd:requirements only contributes when the requirements are met. Which equipped items work is the smallest stable set: items without requirements, then repeatedly every item met by base stats + the bonuses unlocked so far (its own bonus excluded), so items can't unlock each other in a loop. EffectiveStats.holderOf finds the player carrying a stack, for vanilla code that only passes the stack.
  • Attributes: health, movement speed and attack damage/speed are vanilla attribute modifiers. They're recomputed by AbilityEffects.applyAttributeModifiers on join, respawn, equipment change, stat change and /reload, which also adds or removes the enchantment attribute modifiers (Efficiency, …) of equipped items that start or stop working.

World data

Saved data Type What
minednd:limited_items (data/minednd/limited_items.dat, overworld) LimitedItems Per MineDnD item id: copies made naturally (crafting, smithing, brewing, loot) and by operators (/dnd give)
  • No data fixer (SavedDataType with null): Fabric API skips fixing saved data without one.
  • Counts are kept for ids that are no longer limited or no longer exist, so they're right if the item comes back.

Data-driven content

Data Folder Loaded by When Sent to clients
MineDnD items data/<ns>/items/** (old gm_items/ still read) ItemCatalog load, /reload CatalogSyncPayload (hidden items only to operators and creative mode)
Recipes data/<ns>/recipe/** (minednd:crafting_shaped, minednd:brewing, minednd:smithing, minednd:smelting, minednd:smoking, minednd:campfire_cooking, minednd:fletching, minednd:fletching_upgrade, minednd:imbuing) vanilla RecipeManager load, /reload crafting and cooking: vanilla recipe book (finalizeRecipeLoading); brewing: BrewingGuideSyncPayload; fletching: FletchingGuideSyncPayload; imbuing: ImbuingGuideSyncPayload; smithing and fletching upgrades: nothing (found by trying)
Loot injections data/<ns>/loot_injections/** LootInjections load, /reload no
Brewing recipe list every RecipeType.BREWING recipe (vanilla, MineDnD, datapacks) BrewingGuide after start and each /reload BrewingGuideSyncPayload
Fletching recipe list, upgrade slots minednd:fletching recipes, crafting recipes whose result is in #minednd:fletching_table, minednd:fletching_upgrade ingredients FletchingGuide after start and each /reload FletchingGuideSyncPayload
Imbuing recipe list, slot filters minednd:imbuing recipes ImbuingGuide after start and each /reload ImbuingGuideSyncPayload
Requirement rules data/<ns>/crafting_requirements/** CraftingRequirements load, /reload; resolved per item, potion and MineDnD item after start ItemRulesSyncPayload
Potion tags data/<ns>/tags/potion/** (Default Content tiers: #dnd5e:effects, enhanced, harmful) vanilla tags load, /reload vanilla
Translations data/<ns>/lang/<locale>.json DatapackTranslations load, /reload TranslationsSyncPayload
World settings data/minednd/settings.json (all packs, merged key by key over Settings.DEFAULT) SettingsLoader load, /reload SettingsSyncPayload
Enchantments + tags data/<ns>/enchantment/, data/minecraft/tags/enchantment/ vanilla registries world load only vanilla

Notes: - No built-in packs: the jar has no content. The Default Content datapack (datapacks/default-content) is built by gradlew buildDatapack into build/distributions/MineDnD-Default-Content-<version>.zip and attached to every release; players add it to their worlds. The test mod registers a copy as a built-in pack (TestContent, minednd-gametest:default_content), copied there by processGametestResources: on in every world of runClientWithContent, off in the in-game tests. - Test fixture: the in-game tests play with their own pack, src/gametest/resources/resourcepacks/fixture (namespace fixture, minednd-gametest:fixture, on in every test world): the part of the Default Content they were written against, copied once and edited by hand since, so changing or rebalancing the Default Content never breaks an engine test. A test that needs more content adds it to the fixture. DefaultContentClientGameTest checks the real Default Content on its own: a world opened with it (and without the fixture) loads every file, decodes every rule and loot injection, resolves every dnd5e: id and has English and Italian for every text. - Settings: SettingsLoader validates each pack's settings.json on its own, then merges them key by key from the lowest to the highest pack; what no pack sets keeps Settings.DEFAULT. - Item stacks: CatalogEntry keeps an ItemStackTemplate. Stacks can't be built during the reload (item default components aren't bound yet), so they're created on demand. - Catalog recipes: CatalogRecipe extends ShapedRecipe. The pattern and key are vanilla; the result names a catalog id and is looked up in assemble/display. The recipe book works because the server calls RecipeManager.finalizeRecipeLoading after every reload listener, so the catalog is loaded by then. - Base items (BaseItemRule): a MineDnD item is never its base item in a recipe. IngredientMixin makes a plain Ingredient (item, tag, list) never match a catalog stack: vanilla only uses Ingredient in recipes, so this covers every slot of every ingredient recipe, MineDnD's included. RecipeManagerMixin (last-used recipe) and RecipeMapMixin (every search) drop recipes that aren't BaseItemRule.Own when the input holds a catalog stack: this covers special recipes that check items in code (repair, dye, cloning…). InventoryMixin keeps the recipe book from moving catalog stacks. A MineDnD recipe asks for one with CatalogItemIngredient ({ "catalog_item": … }, or the Fabric custom ingredient minednd:catalog_item), matched by catalog id; its items, for slot filters, are the catalog item's base item, looked up when the recipe manager asks (after every reload listener). - Smithing recipes: CatalogSmithingRecipe extends SimpleSmithingRecipe (so the table's slot filters and result work as for smithing_transform); its slots are parsed like crafting keys and its result is a fresh catalog item. SmithingMenuMixin applies craft requirements and limits; SmithingTemplateItemMixin and the client SmithingScreenMixin drop the vanilla template's tooltip lines and slot hints for catalog stacks. - The fletching table: a plain block in vanilla; Fabric's UseBlockCallback opens FletchingMenu, a CraftingMenu with its own menu type and block whose result comes from FletchingRecipes#gridRecipeFor (fletching recipes, else a tagged crafting recipe), with the requirement rules and limits; ResultSlotMixin takes the grid's leftovers from the same recipe. FletchingUpgradeMenu is an ItemCombinerMenu laid out like the smithing table, its slot filters from FletchingGuide (the same on both sides). A tab click sends FletchingTabPayload; the server opens the other menu at the same table. Fletching recipes never go in the recipe book (removed on join from players who had them as crafting recipes). Client: FletchingScreen (crafting table texture, FletchingGuidePanel, ghost recipe), FletchingUpgradeScreen (smithing texture); the tabs and the recipe list are shared with the enchanting table (StationTabs, RecipeListPanel). - The enchanting table's Imbue tab: vanilla's EnchantmentMenu stays the Enchant tab; EnchantmentScreenMixin (client) draws StationTabs.ENCHANTING on it. A tab click sends ImbueTabPayload: the server opens ImbuingMenu at the same table (EnchantmentMenuAccessor gives its access), or the table's own menu again. ImbuingMenu is an ItemCombinerMenu (base, catalyst, lapis, result) that looks up minednd:imbuing recipes, checks the lapis, the rules and limits (CraftingRequirements#mayMake) and the levels (mayPickup), and takes them in onTake; two data slots send the cost to ImbuingScreen, which draws it like the anvil's, with ImbuingGuidePanel. - Cooking recipes: CatalogCookingRecipes has one subclass per vanilla cooking recipe (SmeltingRecipe, SmokingRecipe, CampfireCookingRecipe), so furnaces, smokers and campfires find them under their own recipe type and work unchanged (the campfire casts to CampfireCookingRecipe); assemble builds the catalog item, empty for a limited one (nobody takes it from a furnace, so its limit couldn't be counted). RecipeUnlocks treats them like crafting recipes. - Limited items (LimitedItems): checked where a result is shown (CraftingMenuMixin, SmithingMenuMixin, the brewing stand's isBrewable) and again where it's taken (ResultSlotMixin, SmithingMenuMixin#mayPickup), counted on take, per bottle in doBrew, and when loot drops (CatalogItemEntry.drop). Crafters refuse them; CatalogChests leaves them out; /dnd give counts apart. - Hidden items (HiddenItems): the catalog, item rules and brewing guide are filtered per player (canSee: LEVEL_GAMEMASTERS or creative mode); PlayerListMixin (permission level) and ServerPlayerMixin (setGameMode) resend them when that changes. Hidden items' recipes aren't auto-awarded (RecipeUnlocks), and CatalogChests and generate-content-docs.js leave them out. - Loot: minednd:catalog_item (CatalogItemEntry) is a SingleEntryContainerBase. Loot tables load before reload listeners, so LootTableEvents.MODIFY adds to every table a pool with one minednd:injected entry naming it, which reads LootInjections when loot is rolled; without an injection it draws no random numbers, so vanilla loot is unchanged. - Recipe unlocks (RecipeUnlocks): vanilla advancements without display (advancement/recipes/) with inventory_changed criteria and the recipe in rewards.recipes. Catalog recipes that no advancement rewards are awarded to everyone on join and after /reload. On a player's first join with 0.9.0+ (recipes_relocked not set), catalog recipes whose unlock isn't done are taken back, then inventory_changed fires for every carried stack. - Brewing recipes: brewing is data-driven since 26.3 (minecraft:brewing, run by the stand through RecipeType.BREWING, whose inputs and reagents also fill the slot filters' property sets). CatalogBrewingRecipe extends BrewingRecipe and keeps that type: its result names a catalog id (built in assemble), and its input can be a catalog item (catalog_item, matched by the stack's catalog id). The parent gets placeholders: any potion bottle as input (for the slot filter) and a barrier as output. Brewing recipes have no recipe book display, so the list players browse is BrewingGuide: example stacks per input and reagent, the result and the brewing requirements, sorted by bottle kind then potion registry order. - Requirement rules: - Craft checks need the recipe (requirementsFor). - Place/use checks need per-stack data, which resolveItemRules builds once tags and recipes are bound (after SERVER_STARTED and each successful reload), as a RuleIndex keyed by item, by (bottle item, potion) and by catalog id. A recipe target counts as its result's catalog id, else its potion in that bottle, else its item, so a rule on one MineDnD or brewing recipe doesn't reach every sword or every potion. - Rule targets: recipes OR (items AND potions, whichever are set). Potions alone cover every bottle and tipped arrows. "Craft" also means brewing (RuleAction.verb says "brew" for potions in messages and tooltips). - Brewing: BrewingStandBlockEntityMixin wraps the stand's recipe lookups: a recipe whose requirements the stand's brewer (UUID saved on the block entity, set by AbstractContainerMenuMixin when a player changes the stand's slots) doesn't meet, or whose brewer is offline, counts as no recipe for that bottle. - The highest minimum per ability wins across matching rules.

Item components

Component Record Used by
minednd:stat_bonus StatBonus (bonuses + slot group) EffectiveStats, tooltip
minednd:requirements AbilityRequirements EffectiveStats, EnchantmentHelperMixin, ActiveAbilities, tooltip
minednd:active_ability ActiveAbility (fireball/throw, damage, scaling, costs, trigger) ActiveAbilities, ItemStackMixin, tooltip

All three have a persistent codec and a network stream codec, so they work in datapack JSON, /give and over the network like vanilla components.

Networking

Payload Direction When Content
SettingsSyncPayload server → client join, /reload Settings
CatalogSyncPayload server → client join, /reload, op or game mode change every CatalogEntry (hidden ones only to operators and creative mode)
ItemRulesSyncPayload server → client join, /reload, op or game mode change (key, action, requirements) entries; key = item, (bottle item, potion) or catalog id (hidden items' only to operators and creative mode)
BrewingGuideSyncPayload server → client join, /reload, op or game mode change every brewing recipe: example inputs and reagents, result, requirements (those making or using a hidden item only to operators and creative mode)
FletchingGuideSyncPayload server → client join, /reload, op or game mode change the fletching grid's recipes (cells of example stacks, result, requirements) and the upgrades' ingredients (those with a hidden item only to operators and creative mode)
ImbuingGuideSyncPayload server → client join, /reload, op or game mode change the imbuing recipes: ingredients (for the slot filters), example stacks, lapis and levels, result, requirements (those with a hidden item only to operators and creative mode)
ImbueTabPayload client → server a tab of the enchanting table which tab; the server opens it at the same table if the player can still use it
FletchingTabPayload client → server a tab of the fletching table which tab; the server opens it at the same table if the player can still use it
TranslationsSyncPayload server → client join, /reload locale → key → text
TitleSyncPayload server → client join, /reload every Title (registry-aware codec: the effects' tags)
SetTitlePayload client → server title button in the character sheet optional title id; the server checks it's unlocked
SpendStatPointsPayload client → server Save in the character sheet PointAllocation (points per ability); server re-checks unspent points and the cap, applies all or nothing
UseAbilityPayload client → server ability key (V), gear key (G) equipment slot (a hand, or a worn armor slot); server re-checks slot, item, cooldown, requirements, use rules
  • Player data: stats and progress travel with the attachment sync, not with payloads.
  • Clients without the mod: payloads are only sent to clients that registered them (ServerPlayNetworking.canSend).

Gameplay flows

Ability effects

flowchart LR
  base["PlayerStats (base)"] --> eff["EffectiveStats<br/>+ item bonuses"]
  eff --> ae["AbilityEffects<br/>1 + rate × modifier, floor<br/>(Settings.abilities)"]
  ae --> attr["Attribute modifiers<br/>health · speed · damage · attack speed"]
  ae --> mix["Mixins<br/>log speed · hunger · air control · XP · bow/crossbow/trident timing"]

Requirements

Action Hook Side
Craft CraftingMenuMixin (crafting table and 2×2 grid), SmithingMenuMixin (smithing table), CrafterBlockMixin server
Brew BrewingStandBlockEntityMixin (start and finish of a brew), AbstractContainerMenuMixin (who the brewer is) server
Place BlockItemMixin both (synced rules)
Use item ItemStackMixin (use, useOn; also potions below their own minednd:requirements) both
Use block BlockStateBaseMixin (useItemOn, useWithoutItem) both
Item powers EnchantmentHelperMixin, EffectiveStats, ActiveAbilities both

Item abilities

sequenceDiagram
  participant P as Player
  participant C as Client
  participant S as Server
  P->>C: right click (or V, or G for worn armor)
  C->>C: ItemStackMixin → ActiveAbilities: requirements, hunger (swing or message)
  C->>S: vanilla use packet (right click) / UseAbilityPayload (slot)
  S->>S: same checks (+ cooldown, use rules for the keys)
  S->>S: perform: SpellBolt / ThrownWeapon, or AbilityPowers (buff, imbue, heal, area, blink, leap, lightning)
  S->>S: if it happened: cooldown, durability, exhaustion
  S-->>C: entity spawn, effects, attributes, motion (synced as vanilla)
  • Types: ActiveAbility.Type. Projectiles are made in ActiveAbilities; the rest is AbilityPowers. A power that can't happen (blink with no room, lightning with nothing in reach) returns false and costs nothing.
  • Codec: ActiveAbility.CODEC is two field groups (DFU's record builder takes at most 16 fields): the original ten, then Extra for the fields of the newer types. Effects and attributes are built-in registry holders (holderByNameCodec), so the codec needs no registry context.
  • State: buff boosts are transient attribute modifiers minednd:ability_boost/<attribute> (one per attribute, a newer boost replaces it), ended by the server tick through ability_boosts. imbue hits are counted in ServerLivingEntityEvents.AFTER_DAMAGE (attacker = the player), their extra damage is added after the title bonus in EnchantmentHelperMixin#modifyDamage (melee and projectiles); damage dealt by area and lightning is flagged so it doesn't spend them. leap resets the fall distance every tick until landing (or 10 s).
  • Keys: V uses the hand with a key ability (keyAbilityHand); G the first worn slot, head to feet, with a ready ability (gearAbilitySlot). Worn pieces ignore right_click.
  • Tests: AbilitiesClientGameTest uses every type through useOnKey, checks what it does, and builds every type's tooltip on the client.

Adventure XP

flowchart LR
  orb["ExperienceOrbMixin<br/>(after Mending)"] --> daily
  kill["AFTER_DEATH<br/>first kill of a type<br/>+ max health"] --> first
  kill -- "later kills<br/>repeat_kill_health" --> daily["AdventureXp.grantRepeatable<br/>daily_xp_cap per 24000 ticks"] --> grant
  adv["PlayerAdvancementsMixin<br/>task / goal / challenge"] --> grant
  biome["Server tick /40<br/>biome (visited_biomes), dimension, structure"] --> first
  blocks["PlayerBlockBreakEvents.AFTER<br/>#minednd:first_time_xp"] --> first
  triggers["Advancement trigger mixins<br/>tame, breed, fish, brew, trade, enchant, eat"] --> first
  craft["ItemStackMixin onCraftedBy<br/>LivingEntityMixin onEffectAdded"] --> first
  first["AdventureXp.discover<br/>new id for its kind? (first_times)"] --> grant
  grant["AdventureXp.grant<br/>fractions carried per player"] --> prog["PlayerProgress.addXp<br/>level-ups, points"] --> attach["player_progress attachment<br/>(saved, synced)"]
  death["AFTER_DEATH (player)<br/>death_loss, then first death<br/>from a damage type"] --> attach
  death --> first

Achievements and titles

flowchart LR
  simple["SimpleCriterionTriggerMixin<br/>every simple trigger"] --> repeat["RepeatTrigger.onTrigger<br/>active repeat criteria<br/>(PlayerAdvancements map)"] --> counters["criterion_counters"] --> award
  stats["ServerStatsCounterMixin"] --> statistic["StatisticTrigger"] --> award
  first["AdventureXp.record"] --> discovered["DiscoveredTrigger"] --> award
  cook["FurnaceResultSlotMixin<br/>CampfireBlockEntityMixin"] --> cooked["CookedTrigger"] --> award
  attach["setProgress / setStats"] --> char["LevelTrigger<br/>AbilityScoreTrigger"] --> award
  award["PlayerAdvancements.award"] --> adv["PlayerAdvancementsMixin<br/>XP unless counting,<br/>Titles.refresh"] --> unlocked["unlocked_titles"]
  active["active_title"] --> effects["Titles.* effect helpers<br/>(loot, damage, hunger, health, …)"]
  active --> tag["AvatarRendererMixin<br/>line above the name"]
  • Triggers are registered straight into BuiltInRegistries.TRIGGER_TYPES (CriteriaTriggers#register is private). 26.3 keeps each player's unfinished criteria per trigger (PlayerAdvancements#getTriggerMapForType); repeat reads its own entries there, so only players with repeat criteria pay for the check. ModTriggers.checkAll re-checks the state-based ones on join and after /reload.
  • Titles load like items (reload listener + sync). Unlocked ones aren't stored: Titles.refresh derives them from advancement progress (join, respawn, /reload, award, revoke) and takes off a worn title that's no longer unlocked.
  • Effects read only the worn title. Looting/Fortune raise the level read inside EnchantedCountIncreaseFunction / ApplyBonusCount (max with the real one); Luck of the Sea wraps getFishingLuckBonus in FishingRodItem#use; damage multiplies EnchantmentHelper#modifyDamage; break speed and hunger are in PlayerMixin; health is an attribute modifier; ability bonuses add to EffectiveStats; potion duration scales PotionContents#onConsume.
  • Name tag: the title is copied into AvatarRenderState and submitted one line above the name, after vanilla's name tag, so it follows its visibility (distance, sneaking, invisibility; never your own).

Mixins

Mixin Target Injection Purpose
AbstractContainerMenuMixin AbstractContainerMenu#clicked @Inject HEAD/RETURN brewing stand: the player who changes its slots becomes the brewer
BlockItemMixin BlockItem#place @Inject HEAD place requirements
BlockMixin Block#popExperience @ModifyVariable HEAD INT mining XP
BlockStateBaseMixin BlockStateBase#useItemOn, #useWithoutItem @Inject HEAD block use requirements
BowItemMixin BowItem#releaseUsing → getPowerForTime @ModifyArg DEX bow draw
BredAnimalsTriggerMixin BredAnimalsTrigger#trigger @Inject HEAD first-time XP: breeding
BrewedPotionTriggerMixin BrewedPotionTrigger#trigger @Inject HEAD first-time XP: brewing
BrewingStandBlockEntityMixin BrewingStandBlockEntity#isBrewable, #doBrew → getRecipeFor; loadAdditional/saveAdditional @WrapOperation, @Inject TAIL brewing requirements, item limits (checked, counted per bottle), the saved brewer
BrewingStandMenuAccessor BrewingStandMenu.brewingStand @Accessor the stand behind a menu
CrafterBlockMixin CrafterBlock#getPotentialResults @Inject RETURN no restricted or limited auto-crafting
CraftingMenuMixin CraftingMenu#slotChangedCraftingGrid → getRecipeFor @ModifyExpressionValue craft requirements, item limits
ConsumeItemTriggerMixin ConsumeItemTrigger#trigger @Inject HEAD first-time XP: eating and drinking
CrossbowItemMixin CrossbowItem#getChargeDuration @Inject RETURN DEX crossbow reload
CustomDataAccessor CustomData.tag @Accessor read a stack's catalog id without copying (CatalogEntry.isCatalogStack)
EnchantedItemTriggerMixin EnchantedItemTrigger#trigger @Inject HEAD first-time XP: enchanting table
EnchantmentHelperMixin runIterationOnItem (both variants), getItemEnchantmentLevel, modifyDamage, modifyKnockback, … @Inject HEAD enchantments off below requirements (holder, or the stack's carrier)
ExperienceOrbMixin ExperienceOrb#playerTouch → giveExperiencePoints @WrapOperation adventure XP from orbs
FishingRodHookedTriggerMixin FishingRodHookedTrigger#trigger @Inject HEAD first-time XP: fishing
IngredientMixin Ingredient#test(ItemStack) @Inject HEAD a plain ingredient never matches a MineDnD item
InventoryMixin Inventory#isUsableForCrafting @ModifyReturnValue the recipe book doesn't move MineDnD items
EnchantmentMenuAccessor EnchantmentMenu#access @Accessor the table an enchanting menu is at, to open the Imbue tab there
ItemCombinerMenuMixin ItemCombinerMenu#quickMoveStack → Slot#onTake @WrapOperation a shift-clicked smithing, fletching or imbuing result is counted for its limit
ItemStackMixin ItemStack#use, #useOn, #onCraftedBy; #hurtAndBreak, #processDurabilityChange → isDamageableItem @Inject HEAD; @WrapOperation use requirements, item abilities, first-time XP: crafting; unbreakable off below requirements
LivingEntityMixin LivingEntity#getExperienceReward, #onEffectAdded @Inject RETURN / HEAD INT kill XP, first-time XP: effects
PlayerAdvancementsMixin PlayerAdvancements#award → AdvancementRewards#grant @Inject INVOKE adventure XP from advancements
PlayerListMixin PlayerList#sendPlayerPermissionLevel(ServerPlayer) @Inject TAIL resend hidden items after /op, /deop
PlayerMixin Player#getDestroySpeed, #causeFoodExhaustion, #getFlyingSpeed @Inject / @ModifyVariable STR logs, CON hunger, DEX air control
PotionItemMixin PotionItem#getName, TippedArrowItem#getName @Inject HEAD a potion's or tipped arrow's own item_name wins over "Uncraftable Potion" / "Uncraftable Tipped Arrow"
RecipeManagerMixin RecipeManager#getRecipeFor(…, RecipeHolder) → Recipe#matches @WrapOperation the last used recipe: only MineDnD recipes take MineDnD items
RecipeMapMixin RecipeMap#getRecipesFor @ModifyReturnValue every recipe search: only MineDnD recipes take MineDnD items
ResultSlotMixin ResultSlot#mayPickup (override), #onTake, #onQuickCraft; #getRemainingItems → getRecipeFor override, @Inject HEAD, @WrapOperation crafting: re-check and count limited items (shift-clicks too); the fletching grid's leftovers
ServerPlayerGameModeMixin ServerPlayerGameMode#destroyBlock @Inject HEAD/RETURN remember the block breaker
ServerPlayerMixin ServerPlayer#setGameMode @Inject RETURN resend hidden items after /gamemode
SmithingMenuMixin SmithingMenu#createResult → getRecipeFor; mayPickup (override); onTake @ModifyExpressionValue, override, @Inject HEAD smithing: craft requirements, item limits
SmithingTemplateItemMixin SmithingTemplateItem#appendHoverText @Inject HEAD no vanilla template lines on MineDnD templates
TameAnimalTriggerMixin TameAnimalTrigger#trigger @Inject HEAD first-time XP: taming
TradeTriggerMixin TradeTrigger#trigger @Inject HEAD first-time XP: trading
TridentItemMixin TridentItem#releaseUsing (constant 10) @ModifyConstant DEX trident wind-up
ClientLanguageMixin (client) ClientLanguage#getOrDefault, #has @Inject HEAD datapack translations
AbstractContainerScreenAccessor (client) AbstractContainerScreen @Accessor inventory button position
BrewingStandScreenMixin (client) BrewingStandScreen (init, extractBackground; overrides of extractSlots, extractTooltip, mouseClicked, charTyped, keyPressed, containerTick) @Inject, overrides the brewing recipe list
CreativeModeTabsAccessor (client) CreativeModeTabs.CACHED_PARAMETERS @Accessor rebuild the creative tab
CreateWorldScreenInvoker (client) CreateWorldScreen#onCreate @Invoker example world button
EnchantmentScreenMixin (client) EnchantmentScreen#extractBackground (after the dim), TAIL; #extractRenderState TAIL; #mouseClicked HEAD @Inject the Enchant and Imbue tabs
SmithingScreenMixin (client) SmithingScreen#getTemplateItem, #extractOnboardingTooltips → ItemStack#getItem @WrapOperation no vanilla slot hints for MineDnD templates

Conventions and checks: - Handler methods are prefixed minednd$. - defaultRequire: 1: a mixin that no longer matches its target fails at startup. - To check every mixin at once, temporarily call MixinEnvironment.getCurrentEnvironment().audit() in the client init.

Client

  • Keys: K (character sheet), V (ability of the item in hand), G (ability of worn armor); all rebindable under the "MineDnD" category.
  • Screens:
  • the character sheet (StatsScreen);
  • an inventory button that follows the recipe book;
  • the example world button on Create World;
  • the brewing stand's recipe list (BrewingGuidePanel, added by BrewingStandScreenMixin): styled like the recipe book, it shifts the stand right, draws the chosen recipe faded in the empty slots, and keeps its search box out of the screen's widgets (like vanilla's recipe book) so typing reaches it.
  • Tooltips: stat bonuses (like vanilla attribute modifiers), item requirements, craft/brew/place/use rules and the item ability. MineDnD smithing templates show only their own name and lore (no vanilla template lines, no slot hints in the smithing table).
  • Renderers: ThrownItemRenderer for both projectiles. The spell bolt is a full-bright fire charge; the thrown weapon is drawn as itself.

CI/CD and branches

  • CI (ci.yml) runs by itself only on release pull requests (into main or release/*, where it's a required check); for anything else, e.g. a pull request into rc, run it by hand before merging (Actions tab → CI → Run workflow, on the branch; the result is in the Actions tab and on the branch's latest commit, not in the pull request's checks). It builds the mod, the modpack and the datapack, the docs site (strict, so a broken link fails), the Pack Editor's tests and web build, and a dry run of the Modrinth upload. The jobs are in checks.yml; on a pull request, CI skips them when the commit was already published as a release candidate, and builds only the docs when only docs changed.
  • Release candidates (rc.yml, by hand on rc): the same checks, then the jar as X.Y.Z-rc+<commit> on Modrinth (Alpha), replacing the previous candidate. See Releasing.
  • A pull request rc → main is a release: its release-check job requires a new mod_version and a ## [X.Y.Z] section in CHANGELOG.md. On merge, release.yml tags, builds, publishes the GitHub release and uploads to Modrinth (deleting the version's release candidate there).
  • By hand only: the in-game tests (game-tests.yml, on the branch you pick). The Pack Editor downloads (pack-editor.yml) run with every release and, if asked, every release candidate; by hand too (see Releasing).
  • Docs site: docs.yml publishes it to Cloudflare Pages on pushes to main that change the docs.

Branches, tags and the full release steps: Releasing.

Testing

  • Automated:
  • CI builds the mod, the modpack, the datapack and the docs site;
  • the Modrinth dry run checks the upload data.
  • In-game tests: gradlew runClientGameTest (Fabric client game tests, src/gametest) starts a real game, creates a world and plays scenarios with real clicks and commands: e.g. BrewingClientGameTest checks the recipe list and rules reach the client, that the stand brews only what the brewer's scores allow (vanilla and MineDnD potions), and the recipe panel (open, select, tabs, search, Italian). Screenshots land in build/run/clientGameTest/screenshots.
  • What they play with: the fixture pack, never the Default Content (see Test fixture in Data-driven content); DefaultContentClientGameTest checks the Default Content loads cleanly.
  • How long each takes: every test extends TimedClientGameTest (write run, not runTest), which records its time. After the run (even a failed one) Gradle prints them, slowest first, and adds them to build/reports/game-test-timings.csv (date, commit, test, milliseconds, result), the history of every run. Most of the time goes to waiting for the game, e.g. a brew (20 s) or a campfire (30 s). Wait for the result with server.waitFor(…) rather than a fixed waitTicks, and skip waits that aren't what's tested: BrewingClientGameTest checks a brew starts and then sets its timer to the last tick (BrewingStandMenu#setData), and checks a refused brew never starts instead of waiting it out; AchievementsClientGameTest starts the campfire steak near the end (/data merge block … {CookingTimes:[I;595,0,0,0]}); FoodClientGameTest checks the campfire's recipes directly. The game's own code still finishes each one. The whole run takes about 1 min 45 s. CI doesn't run them: run the Game tests workflow (game-tests.yml) by hand on the branch to check (Actions tab → Game tests → Run workflow → Use workflow from: rc, main, …; or gh workflow run game-tests.yml --ref rc). It plays them on a virtual display and keeps the screenshots with the run.
  • In game, during development:
  • quick play the dev world (gradlew runClient --args="--quickPlaySingleplayer <world>") and read the log;
  • for features that need actions (crafting, kills, advancements, screens), add a temporary hook behind a flag file. It runs the action through the real game APIs, logs the result or takes a screenshot (Screenshot.grab), and is removed before committing.

Conventions

  • Code reads like the surrounding code: comments explain why, records are immutable, names describe behaviour.
  • Texts: every player-visible text is a translation key in assets/minednd/lang/en_us.json and it_it.json, with the same keys in both. Datapack texts go in the pack's data/<ns>/lang/.
  • Changelog and docs: every change that players or server owners notice gets a CHANGELOG.md entry under [Unreleased], and the docs are updated in the same pull request: the user-facing pages in both docs/en/ and docs/it/ (same file names; see the index), the developer pages in English only.
  • Settings: after adding or changing a setting in Settings.java, update src/main/resources/minednd/default_settings.json (InfoClientGameTest fails while its values differ from Settings.DEFAULT) and the settings in datapacks.md (both languages): the full file and the field's range.
  • Default Content datapack: after changing datapacks/default-content, regenerate what derives from it:
  • node tools/generate-content-docs.js: content.md, items.md and enchantments.md in docs/en/ and docs/it/. Never edit those by hand. If an enchantment is added, also add its per-level text, in both languages, to effects in that script. It reads Minecraft's names from tools/data/vanilla-names.json; after a Minecraft update, rewrite that with node tools/generate-vanilla-names.js (build the mod and start the client once first).

Documentation site

mkdocs.yml turns docs/ into the website (MkDocs Material): the i18n plugin builds docs/en/ at the root and docs/it/ under /it/, with a language switcher; English-only pages also appear in the Italian site. The pages stay plain Markdown that reads the same on GitHub: docs/hooks/links.py points links to files outside docs/ (the changelog, src/, tools/) to GitHub, and English → Italian links to the Italian site. Anchors are made like GitHub's (accented letters kept), so #links work in both places.

Preview it with a live reload while editing (needs Python 3.9+): ./gradlew runDocs serves it until Ctrl+C (-Pport=8001 for another port), and ./gradlew buildDocs builds it strictly into site/, as CI does. Both set up .venv/ with docs/requirements.txt the first time. -Pchannel=rc gives either one the release candidate's site (DOCS_CHANNEL=rc: its address as site_url and the "next version" notice in the bar; docs/hooks/version.py, docs/overrides/main.html, and the two addresses in extra.channels of mkdocs.yml). Or in the background, with the script:

tools/docs-site.sh start    # the first time it sets up .venv/ with docs/requirements.txt
tools/docs-site.sh status
tools/docs-site.sh stop

It serves http://127.0.0.1:8000/ (Italian: /it/) in the background, and the page reloads by itself when a file in docs/ is saved. PORT=8001 picks another port; the server's output is in .venv/docs-site.log. By hand: .venv/bin/mkdocs serve.

mkdocs build is strict: a broken link or anchor fails it, in CI too. A new page needs a line in nav in mkdocs.yml. Publishing: Releasing.

MineDnD Pack Editor

tools/pack-editor/ is the MineDnD Pack Editor desktop app: Tauri 2 (a small Rust shell with the dialog and file plugins, src-tauri/) around a TypeScript page built with Vite, with a CodeMirror editor. Needs Node.js 24 and, for the app itself, Rust (brew install rustup && rustup default stable; on Linux also WebKitGTK, see pack-editor.yml).

cd tools/pack-editor
npm install
npm run dev        # the page alone in a browser, http://localhost:1420 (Open and Save need the app)
npm run app        # the app, reloading as you edit
npm test           # the checks against every file in datapacks/, ranges against the Java codecs, translations
npm run typecheck
npm run dist       # the portable app for this system (macOS: bundle/macos/*.app, Linux: bundle/appimage/; Windows: --no-bundle, target/release/*.exe)

From the project root, ./gradlew runPackEditor does the same as npm install and npm run app.

Path Contents
src/validate/ The checks: core.ts (field checks, "did you mean"), schemas.ts (one per file type, mirroring the codecs), formula.ts (a port of XpFormula), index.ts (type detection, JSON positions)
src/pack/ Whole packs: pack.ts (zip and folder, new packs, adding, renaming, saving, the pack's own checks), references.ts (checks across files: ids a file names that no file defines, missing texts, namespaces), tree.ts (the file tree), templates.ts (new-file templates)
src/preview/ The previews: index.ts (one per file type), text.ts (text components with Minecraft colours)
src/i18n.ts Every UI text and problem message, in English and Italian
src/data.ts Names from tools/data/vanilla-names.json and the mod's lang files
src/main.ts, src/editor.ts The window: editor, problems, preview; opening and saving files
src/menu.ts The native menus (Tauri): the menu bar main.ts describes, and the file tree's right-click menu
src/channel.ts Release or release candidate build (VITE_MINEDND_CHANNEL=rc): the app's name and the docs site its Guide opens
src-tauri/tauri.rc.conf.json What the release candidate's build changes in tauri.conf.json: name "MineDnD Pack Editor RC", its own app id

The checks are written by hand from the Java codecs, so when a codec changes, change schemas.ts too. The tests catch part of it: they read the ranges in ActiveAbility.java and Settings.java and check the Pack Editor uses the same, and every file in datapacks/ must have no problems. The app's version is the mod's (npm run dist copies mod_version into package.json and Cargo.toml).

The release candidate's build (what Pack Editor downloads makes on rc): VITE_MINEDND_CHANNEL=rc npm run dist -- --config src-tauri/tauri.rc.conf.json.

Extending: checklists

A new ability effect 1. Add the rate and floor to a Settings record (codec field + default). 2. Add a method in AbilityEffects, using effective stats. 3. Apply it: an attribute modifier in applyAttributeModifiers, or a mixin. Run it on both sides if the client predicts it. 4. Add it to src/main/resources/minednd/default_settings.json (InfoClientGameTest fails until it matches). 5. Update the docs (gameplay.md for the rule, datapacks.md for the setting, in docs/en/ and docs/it/) and the changelog.

A new item component 1. Add a record with a codec and a stream codec in models. 2. Register it in ModComponents. 3. Read it where it applies, and add a tooltip line if players should see it. 4. Document its fields in datapacks.md (both languages).

A new datapack folder 1. Write a SimpleReloadListener (ResourceLoader.get(SERVER_DATA).registerReloadListener). 2. If clients need it, add a payload, sync it on join and after /reload, and clear it on disconnect. 3. Document it in datapacks.md (both languages).

A new payload 1. Write a record with a Type and a StreamCodec. 2. Register it with PayloadTypeRegistry.clientboundPlay() or serverboundPlay(). 3. Validate everything server-side for client → server payloads.