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+.
- 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 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 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.
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#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, 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 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.
- Renderers:
ThrownItemRendererfor 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 torcand 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→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. - 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.ymlby 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.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. 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. - Studio UI:
STUDIO_AUTOMATION=<steps.json>makes the app run JavaScript steps in its window and save screenshots (seeelectron/main.ts);npm run dev:sftpserves 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.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, updateSETTINGS_FIELDSintools/minednd-studio/src/model/settings.ts(a test compares names, defaults and ranges), add its texts to bothsrc/i18nfiles, and runnpm run generate:settingsthere 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:contentintools/minednd-studio: Studio's embedded copy. A test fails while it's out of date.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 from Studio'svanilla.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.