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). MineDnD Studio also needs Node.js 22.18+.

  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 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 studio / studioTest Builds and opens MineDnD Studio / runs its tests

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 (runClient): 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 MineDnD Studio (Electron, React, TypeScript, Vite; Node.js 22.18+ to build and test it); GitHub Actions for CI and releases

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/resourcepacks/default_content/ The built-in datapack (items, recipes, enchantments, tags, rules, translations)
src/main/resources/resourcepacks/settings/ The built-in "MineDnD World Settings" datapack: settings.json with every default (generated)
tools/minednd-studio/ MineDnD Studio, the desktop datapack editor: app, tests, data generators, installer config
modpack/ Extras for the .mrpack (Modrinth files list + config overrides)
modrinth/ Modrinth listing texts
.github/ CI and Release 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, 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, mirrored in Studio's formula.ts) and SettingsLoader
catalog MineDnD items: ItemCatalog (server), ClientCatalog, CatalogEntry
crafting Requirement rules (CraftingRequirements, ItemRules, RuleAction) and the CatalogRecipe recipe type
entity Ability projectiles: SpellBolt, ThrownWeapon
loot The minednd:ability_scores condition
i18n Translations from datapacks
network Payloads between server and client
registry Attachments, item components, entities, creative tab, condition types
command /dnd commands
world Item chests and the example world
mixin Hooks into vanilla code

Architecture

flowchart LR
  subgraph Data["Datapacks (built-in + world)"]
    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 (built-in pack + 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: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.

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
Recipes data/<ns>/recipe/** (minednd:crafting_shaped, minednd:brewing) vanilla RecipeManager load, /reload crafting: vanilla recipe book (finalizeRecipeLoading); brewing: BrewingGuideSyncPayload
Brewing recipe list every RecipeType.BREWING recipe (vanilla, MineDnD, datapacks) BrewingGuide after start and each /reload BrewingGuideSyncPayload
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/** (built-in tiers: #minednd: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; minednd:settings is the base) SettingsLoader load, /reload SettingsSyncPayload
Enchantments + tags data/<ns>/enchantment/, data/minecraft/tags/enchantment/ vanilla registries world load only vanilla

Notes: - Built-in packs: ResourceLoader.registerBuiltinPack(minednd:default_content | minednd:settings, DEFAULT_ENABLED). Existing worlds pick them up automatically. World packs above default_content override files with the same path. settings.json is the exception: SettingsLoader validates each pack's file on its own, then merges them with minednd:settings always at the bottom, so its full set of defaults never hides a smaller file from another pack. - 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. - 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 every CatalogEntry
ItemRulesSyncPayload server → client join, /reload (key, action, requirements) entries; key = item, (bottle item, potion) or catalog id
BrewingGuideSyncPayload server → client join, /reload every brewing recipe: example inputs and reagents, result, requirements
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) hand; server re-checks 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), 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)
  C->>C: ItemStackMixin → ActiveAbilities: requirements, hunger (swing or message)
  C->>S: vanilla use packet (right click) / UseAbilityPayload (V)
  S->>S: same checks (+ cooldown, use rules for V)
  S->>S: spawn SpellBolt / ThrownWeapon, cooldown, durability, exhaustion
  S-->>C: entity spawn + synced data (projectile rendered as its item)

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, the saved brewer
BrewingStandMenuAccessor BrewingStandMenu.brewingStand @Accessor the stand behind a menu
CrafterBlockMixin CrafterBlock#getPotentialResults @Inject RETURN no restricted auto-crafting
CraftingMenuMixin CraftingMenu#slotChangedCraftingGrid → getRecipeFor @ModifyExpressionValue craft requirements
ConsumeItemTriggerMixin ConsumeItemTrigger#trigger @Inject HEAD first-time XP: eating and drinking
CrossbowItemMixin CrossbowItem#getChargeDuration @Inject RETURN DEX crossbow reload
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
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
PlayerMixin Player#getDestroySpeed, #causeFoodExhaustion, #getFlyingSpeed @Inject / @ModifyVariable STR logs, CON hunger, DEX air control
PotionItemMixin PotionItem#getName @Inject HEAD a potion's own item_name wins over "Uncraftable Potion"
ServerPlayerGameModeMixin ServerPlayerGameMode#destroyBlock @Inject HEAD/RETURN remember the block breaker
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

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 (item ability); both are 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.
  • Renderers: ThrownItemRenderer for both projectiles. The spell bolt is a full-bright fire charge; the thrown weapon is drawn as itself.

MineDnD Studio (tools/minednd-studio/)

A desktop app (Electron main process in electron/, React window in src/) to edit datapacks on this computer or on a server. Its pure model (src/model/) mirrors the mod's codecs and formulas, and tests fail when the two drift. Layout, commands and the sync checklist: its README. Using it: the guide.

CI/CD and branches

  • CI (ci.yml) builds every push to rc and every pull request: the mod, MineDnD Studio's type check, tests and build, and a dry run of the Modrinth upload.
  • 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.
  • By hand only: the in-game tests (game-tests.yml, on the branch you pick) and the Studio installers (studio.yml, for a release tag or as run artifacts).

Branches, tags and the full release steps: Releasing.

Testing

  • Automated:
  • CI builds the mod;
  • MineDnD Studio's tests cover its model, translations and file backends (see MineDnD Studio);
  • the Modrinth dry run checks the upload data;
  • the Studio installers aren't built by CI: run studio.yml by hand after changing how Studio is packaged.
  • 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. 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.
  • Studio UI: STUDIO_AUTOMATION=<steps.json> makes the app run JavaScript steps in its window and save screenshots (see electron/main.ts); npm run dev:sftp serves a folder over SFTP to try the remote mode.

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 SETTINGS_FIELDS in tools/minednd-studio/src/model/settings.ts (a test compares names, defaults and ranges), add its texts to both src/i18n files, and run npm run generate:settings there to rewrite the built-in settings pack (a test fails while it's out of date).
  • Built-in content: after changing resourcepacks/default_content, regenerate what derives from it:
  • npm run generate:content in tools/minednd-studio: Studio's embedded copy. A test fails while it's out of date.
  • 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 Studio's vanilla.json.

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+):

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.

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 the field to the editor's SETTINGS_FIELDS. The sync test fails until defaults match. 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. In MineDnD Studio: parse and serialize it in src/model/item.ts (so it isn't kept as an unknown component), add its fields to editors/ItemEditor.tsx, its texts to both src/i18n files, and tests.

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.