For AI agents
This page is for language models (and the people pointing them here). It says where to find everything and the rules a Myriad addon has to follow. Read it first.
Reading these docs
/llms.txtlists every page with a summary./llms-full.txtis all of them in one file. Every page is also plain Markdown at its address plus.md:/docs/develop/hooks.md.- The API’s javadoc ships with
dev.myriadclient:myriad(-sources.jarand-javadoc.jaronhttps://maven.myriadclient.dev/releases). Every public class and method is documented: read it when these pages don’t cover a detail. - Working code to copy patterns from: the addon template , the example addon , and Essentials .
What to build, in order
- Start from the template; rename the mod id, package and main class (Setting up).
- Write modules: Modules, Settings, Events.
- Act through Services, never raw packets, when a service covers it.
- Add windows and HUD elements if needed (UI), commands (Commands).
- Only if the task is to change how Myriad or another addon decides something: Hooks.
- Build with
./gradlew build; test with the dev client andselftest(Testing).
Rules
These are not style preferences. An addon that breaks them breaks for players.
- Only import
dev.myriadclient.api.*. Neverdev.myriadclient.impl.*: it’s internal and changes every release. - Never implement an interface marked
@ApiStatus.NonExtendable(services,AddonContext,Window,Canvas,Hooks…). To replace a service, extend itsForwardingclass. - Register everything in
MyriadAddon.initializethrough theAddonContext. Categories inregisterCategories. Optional integrations inpostInitialize. - No static instances or static state. Shared state is an object made in
initializeand passed to constructors. Reach a module withModules.active(MyModule.class)(null while off). - Act in
TickEvent.Pre(clicks, placing, breaking, attacking, slot switches), or through a service that does. - Rotate through
Myriad.rotations(), switch slots throughMyriad.inventory(), place throughMyriad.placement(), break throughMyriad.breaking(). Passthis(the module) as the owner. - Use
Options.forServer()for services’ options. Don’t add a “Strict”/“Grim” setting. - Settings come from groups (
sgGeneral.bool("Name")...build()), with adescription. Colours default to theme roles (SettingColor.role(SettingColor.Mode.ACCENT)). .protect()every private setting: URLs with tokens, webhooks, passwords, coordinates, server addresses, file paths, free text that could hold them.- Never write another module’s settings to change its behaviour. Lay an override
(
setting.override(this, Setting.Layer.MODULE, "Label", value)) andrelease(this)when done. - Don’t depend on Essentials at runtime. To use its hooks,
compileOnlyit and register behindFabricLoader.getInstance().isModLoaded("myriad-essentials"), in a separate class. - Mixins stay thin: they call a method on the module, which checks
Modules.active(...). Prefix handler names with the mod id. Prefer an event when one exists. - Hooks are for decisions: use
ctx.hook(...)only to answer a decision; callcall.proceed()when you have no answer; scope with the narrowestMatch. A replacement scoped to your addon hands calls on withsuper. - Ticks are cheap, frames are not: keep render handlers light; cache per tick (
TickCached) or per chunk (ChunkCache). - Keep the jar’s
fabric.mod.jsoncorrect:entrypoints.myriad,depends.myriad(the oldest supported),environment: client,contact.sourcesfor updates.
Common mistakes
| Mistake | Instead |
|---|---|
mc.player.connection.send(new ServerboundMovePlayerPacket.Rot(...)) | Myriad.rotations().request(this, yaw, pitch, priority) |
mc.player.getInventory().selected = slot | Myriad.inventory().select(slot) or silentSwap(slot, action) |
a static MyModule INSTANCE | Modules.active(MyModule.class) |
otherModule.someSetting.set(x) then setting it back | someSetting.override(this, Setting.Layer.MODULE, "Mine", x) / release(this) |
a webhook StringSetting without .protect() | add .protect() |
attacking in TickEvent.Post or a render handler | attack in TickEvent.Pre |
a hook handler that returns a value without proceed() “just in case” | proceed() unless you mean to take over |
Match.always() for a replacement meant for your modules | Match.addon(ctx.namespace()) |
A complete, correct module
package com.example.addon.modules;
import dev.myriadclient.api.Myriad;
import dev.myriadclient.api.combat.TargetSettings;
import dev.myriadclient.api.combat.Targets;
import dev.myriadclient.api.event.Subscribe;
import dev.myriadclient.api.event.events.TickEvent;
import dev.myriadclient.api.module.Categories;
import dev.myriadclient.api.module.Module;
import dev.myriadclient.api.service.Rotations;
import dev.myriadclient.api.setting.DoubleSetting;
import dev.myriadclient.api.util.Interactions;
import net.minecraft.world.entity.Entity;
/** Faces the nearest target in range and hits it when the attack is charged. */
public final class SimpleAura extends Module {
private final DoubleSetting range = sgGeneral.doubleSetting("Range")
.description("How far to reach for a target.")
.defaultValue(3.0).range(1, 6).decimals(1)
.build();
private final TargetSettings targets = new TargetSettings(settings, Targets.Type.PLAYERS, Targets.Type.HOSTILES);
public SimpleAura() {
super(Categories.COMBAT, "Simple Aura", "Hits the nearest target in range.");
}
@Subscribe(inGame = true)
private void onTick(TickEvent.Pre e) {
Entity target = targets.best(range.get());
if (target == null) return;
float[] aim = Myriad.rotations().anglesTo(target.getBoundingBox().getCenter());
Myriad.rotations().request(this, aim[0], aim[1], Rotations.PRIORITY_NORMAL, Rotations.Options.forServer(), null);
// Hit once the server faces it and the attack is charged (Interactions.attack also swings).
if (Myriad.inventory().attackCharge() >= 1 && Myriad.rotations().isFacing(aim[0], aim[1], 15)) {
Interactions.attack(target, true);
}
}
}