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).
- Open the project folder in IntelliJ and let it import the Gradle project (the first import downloads Minecraft, a few minutes).
- 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 useoptionalFieldOfwith a default (e.g.xp), so old saves keep loading. - Effective stats (
EffectiveStats) = base +minednd:stat_bonusof equipped items per slot group. An item withminednd:requirementsonly 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.holderOffinds 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.applyAttributeModifierson 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 (
SavedDataTypewithnull): 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 inActiveAbilities; the rest isAbilityPowers. A power that can't happen (blink with no room, lightning with nothing in reach) returns false and costs nothing. - Codec:
ActiveAbility.CODECis two field groups (DFU's record builder takes at most 16 fields): the original ten, thenExtrafor the fields of the newer types. Effects and attributes are built-in registry holders (holderByNameCodec), so the codec needs no registry context. - State:
buffboosts are transient attribute modifiersminednd:ability_boost/<attribute>(one per attribute, a newer boost replaces it), ended by the server tick throughability_boosts.imbuehits are counted inServerLivingEntityEvents.AFTER_DAMAGE(attacker = the player), their extra damage is added after the title bonus inEnchantmentHelperMixin#modifyDamage(melee and projectiles); damage dealt byareaandlightningis flagged so it doesn't spend them.leapresets 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 ignoreright_click. - Tests:
AbilitiesClientGameTestuses every type throughuseOnKey, 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#registeris private). 26.3 keeps each player's unfinished criteria per trigger (PlayerAdvancements#getTriggerMapForType);repeatreads its own entries there, so only players with repeat criteria pay for the check.ModTriggers.checkAllre-checks the state-based ones on join and after /reload. - Titles load like items (reload listener + sync). Unlocked ones aren't stored:
Titles.refreshderives 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 wrapsgetFishingLuckBonusinFishingRodItem#use; damage multipliesEnchantmentHelper#modifyDamage; break speed and hunger are inPlayerMixin; health is an attribute modifier; ability bonuses add toEffectiveStats; potion duration scalesPotionContents#onConsume. - Name tag: the title is copied into
AvatarRenderStateand 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 byBrewingStandScreenMixin): 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:
ThrownItemRendererfor 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 (intomainorrelease/*, where it's a required check); for anything else, e.g. a pull request intorc, 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 inchecks.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 onrc): the same checks, then the jar asX.Y.Z-rc+<commit>on Modrinth (Alpha), replacing the previous candidate. See Releasing. - A pull request
rc→mainis a release: itsrelease-checkjob requires a newmod_versionand a## [X.Y.Z]section inCHANGELOG.md. On merge,release.ymltags, 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.ymlpublishes it to Cloudflare Pages on pushes tomainthat 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.BrewingClientGameTestchecks 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 inbuild/run/clientGameTest/screenshots. - What they play with: the fixture pack, never the Default Content (see Test fixture in Data-driven content);
DefaultContentClientGameTestchecks the Default Content loads cleanly. - How long each takes: every test extends
TimedClientGameTest(writerun, notrunTest), which records its time. After the run (even a failed one) Gradle prints them, slowest first, and adds them tobuild/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 withserver.waitFor(…)rather than a fixedwaitTicks, and skip waits that aren't what's tested:BrewingClientGameTestchecks 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;AchievementsClientGameTeststarts the campfire steak near the end (/data merge block … {CookingTimes:[I;595,0,0,0]});FoodClientGameTestchecks 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, …; orgh 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.jsonandit_it.json, with the same keys in both. Datapack texts go in the pack'sdata/<ns>/lang/. - Changelog and docs: every change that players or server owners notice gets a
CHANGELOG.mdentry under[Unreleased], and the docs are updated in the same pull request: the user-facing pages in bothdocs/en/anddocs/it/(same file names; see the index), the developer pages in English only. - Settings: after adding or changing a setting in
Settings.java, updatesrc/main/resources/minednd/default_settings.json(InfoClientGameTestfails while its values differ fromSettings.DEFAULT) and the settings indatapacks.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.mdandenchantments.mdindocs/en/anddocs/it/. Never edit those by hand. If an enchantment is added, also add its per-level text, in both languages, toeffectsin that script. It reads Minecraft's names fromtools/data/vanilla-names.json; after a Minecraft update, rewrite that withnode 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.