LethalBreed siege manual Addon API Manual Français

Addon API

Another mod reaches LethalBreed through one entry point and a handful of small classes and packages, all under oas.dreyka.lethalbreed.api. Nothing here needs a mixin: an addon declares options, listens for events, reads what the mod currently believes, and adds its own special zombies, through calls that stay stable across a release the way the rest of this package's javadoc already promises.

The surface, and what each class is for
ClassWhat it is for
LethalBreedAddonyour entry point, called twice at startup
LethalBreedApideclare an AI namespace, listen for a phase change
LethalBreedConfigApiregister your own options in the same config file
LethalBreedStateread-only: phase, tracked count, a zombie's variant, packs
api.event.*six Fabric events: zombie adoption, contamination, mood, pack join, spawn cull, targeting
api.variant.*add a special zombie of your own, passive, active or on death

Depending on the mod

There is no Maven repository to point at: the jar published on Modrinth or CurseForge is the one an addon compiles against. Drop it in a folder of the project and declare it with modImplementation, never compileOnly and never plain implementation, or Loom leaves it unremapped and the compiler starts talking about class_1799 instead of the classes you expected. Fabric API goes on the same configuration, because the mod's events are Fabric Event objects and one is needed at compile time.

build.gradle.ktsplugins {
    id("fabric-loom") version "1.17.12"
}

repositories {
    mavenCentral()
    maven("https://maven.fabricmc.net/")
}

dependencies {
    minecraft("com.mojang:minecraft:1.21.11")
    mappings(loom.officialMojangMappings())
    modImplementation("net.fabricmc:fabric-loader:0.19.5")
    modImplementation("net.fabricmc.fabric-api:fabric-api:0.141.6+1.21.11")

    modImplementation(files("libs/lethalbreed-2.0.0.jar"))
}

In Groovy the block transliterates as is, with one Gradle rule in the way: a plugin version there has to stay a literal, so id 'fabric-loom' version '1.17.12' and never a property. The modid for the depends block of your fabric.mod.json is lethalbreed.

The entry point

LethalBreedAddon is a functional interface with one required method and one optional one, called at two different moments of startup. Declare the implementing class under the lethalbreed:addon entrypoint of your own fabric.mod.json:

fabric.mod.json"entrypoints": {
  "lethalbreed:addon": ["com.example.mymod.MyAddon"]
}

onLethalBreedReady() runs before the config file is read and before anything reads a registry: this is where LethalBreedConfigApi.register goes, along with any allowed AI namespace and phase listener, early enough to be known before the first zombie loads. onConfigLoaded() is a default empty method, called once the file has actually been read and clamped; override it only when a decision of yours depends on the real value of an option rather than its default.

A whole addon

Everything below compiles against the package as it stands: an allowed namespace, an option registered next to this mod's own, a phase listener, a special variant, and one event listener that spares a boss from the mod's spawn cull.

MyAddon.javapackage com.example.mymod;

import oas.dreyka.lethalbreed.api.LethalBreedAddon;
import oas.dreyka.lethalbreed.api.LethalBreedApi;
import oas.dreyka.lethalbreed.api.LethalBreedConfigApi;
import oas.dreyka.lethalbreed.api.OptionBounds;
import oas.dreyka.lethalbreed.api.event.SpawnCullCallback;
import oas.dreyka.lethalbreed.api.variant.SpecialVariant;
import oas.dreyka.lethalbreed.api.variant.SpecialVariantRegistry;

public final class MyAddon implements LethalBreedAddon {

    @Override
    public void onLethalBreedReady() {
        LethalBreedApi.allowAiNamespace("com.example.mymod.");

        LethalBreedConfigApi.register("mymod", "MyMod", MyOptions.class,
                new OptionBounds("mymodSpawnChance", 0.0, 1.0));

        LethalBreedApi.onPhaseChanged((from, to) ->
                MyMod.LOGGER.info("phase moved from {} to {}", from, to));

        SpecialVariantRegistry.register(SpecialVariant.of("mymod:frostbite",
                SpecialVariant.Kind.ACTIVE, 10, new FrostbiteBehavior()));

        SpawnCullCallback.EVENT.register((entity, phase, proposed) ->
                entity.getType() != MyEntities.SWAMP_BOSS && proposed);
    }
}
MyOptions.javapackage com.example.mymod;

public final class MyOptions {
    public static double mymodSpawnChance = 0.2;
}

Every field on the holder class has to start with the claimed prefix, checked rather than trusted: a stray field that missed it would otherwise land in the Misc tab beside a stranger's option. The prefix itself is at least three characters, and taking one already claimed, or one that would capture an option that already exists, throws IllegalArgumentException before anything is written down.

register takes its bounds as a varargs tail: pass as many OptionBounds as you like, one per option worth clamping, and an option you do not name there passes through unclamped. The config screen labels a row lethalbreed.option.<fieldName> and a tab lethalbreed.category.<category>, each falling back to the raw name. The category goes in exactly as it was passed, case included: the shipped tabs are written lethalbreed.category.Perf and lethalbreed.category.Compute, so a register("mymod", "MyMod", ...) is named in lethalbreed.category.MyMod, never under the prefix. Both keys go in the addon's own lang file, under the lethalbreed. namespace: the game merges lang files across mods, so nothing here has to be touched.

Reading what the mod knows

LethalBreedState is the read-only half of the surface: nothing on it changes anything, and nothing on it needs a callback registered first. Everything crossing the boundary is a vanilla type, a primitive, or LethalBreedState.Pack, so an addon compiled against it keeps compiling when the mod's own internal state classes change shape. Server thread only, same as the rest of the mod.

reading stateint phase = LethalBreedState.phase();
int tracked = LethalBreedState.trackedZombieCount();
SpecialVariant variant = LethalBreedState.variantOf(zombie);
List<LethalBreedState.Pack> packs = LethalBreedState.packs(level);
LethalBreedState, method by method
MethodAnswers
phase()the night progression, zero on a fresh world, no upper bound
trackedZombieCount()zombies the mod is driving, every dimension counted together
variantOf(Zombie)the special variant it carries, or null for an ordinary one
packs(ServerLevel)every pack in that dimension, as a snapshot taken now

Events

Six Fabric events, each with a single question and a chained answer: a listener is handed the verdict as it stands and returns the one it wants, so one with no opinion about a given case returns proposed and changes nothing. A listener that throws is reported once, in the server log, and treated from then on as if it had abstained; the mod does not stop for a broken addon.

The six events, and what each decides
EventMethodDecides
ContaminationCallbackboolean allowContamination(LivingEntity, boolean)whether a creature catches the plague
MoodCallbackMoodState mood(Zombie, MoodState)which of the five moods a zombie holds for one activation
PackJoinCallbackboolean allowJoin(Zombie, long, boolean)whether a zombie is let into a pack, pack id 0 (PackJoinCallback.FOUNDING) means founding rather than joining
SpawnCullCallbackboolean allowCull(Entity, int, boolean)whether a hostile is culled (removed); return true to cull it, false to let it survive
TargetCandidateCallbackboolean isValidPrey(Mob, LivingEntity, boolean)whether one creature is prey for one zombie
ZombieAdoptCallbackboolean allowAdopt(Zombie, boolean)whether a zombie is taken over by the mod's brain; returning false leaves it entirely to vanilla

ZombieAdoptCallback is the way out for an addon with a zombie of its own. Everything that loads as a Zombie is taken over: husk, drowned, zombie villager, and whatever subclass another mod wrote. That is the right default, since the whole horde is what the mod is for, but a creature written to do something else ends up with its goals stripped, its navigation driven by the flow field and a pack marching it away. Refusing is total: the zombie keeps its own AI, joins no pack, is never scheduled. It is still a hostile in a world where the mod culls hostiles, so that refusal almost always comes with a SpawnCullCallback. The question is asked on every load rather than once in the life of the world, because the addon that claimed it may well have been absent the time before.

TargetCandidateCallback fires on the broad phase. Several hundred times a tick on a busy server, inside the scan that already prices a horde. Read an attachment, compare an entity type, return: anything that allocates, walks a chunk or takes a lock belongs somewhere else. A datapack that only wants a fixed list of entity types spared from the spawn cull wants the lethalbreed:spawn_protected tag instead, no code required.

The types that cross the boundary

The signatures above name their types without a package, and guessing a game type's package costs a compile: Zombie moved in 1.21 and now lives under monster.zombie. The exact imports, mod side and game side, for the six events, the variants and the state read:

importsimport oas.dreyka.lethalbreed.api.LethalBreedState;
import oas.dreyka.lethalbreed.api.event.ContaminationCallback;
import oas.dreyka.lethalbreed.api.event.MoodCallback;
import oas.dreyka.lethalbreed.api.event.MoodState;
import oas.dreyka.lethalbreed.api.event.PackJoinCallback;
import oas.dreyka.lethalbreed.api.event.SpawnCullCallback;
import oas.dreyka.lethalbreed.api.event.TargetCandidateCallback;
import oas.dreyka.lethalbreed.api.event.ZombieAdoptCallback;
import oas.dreyka.lethalbreed.api.variant.VariantBehavior;
import oas.dreyka.lethalbreed.api.variant.VariantContext;

import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.monster.zombie.Zombie;

The subclasses followed Zombie into the same folder: Husk, Drowned and ZombieVillager live under net.minecraft.world.entity.monster.zombie as well, which matters to an addon sorting by type inside an adoption listener.

MoodState is an enum of five constants, NORMAL, FLEEING, SHELTERING, CELEBRATING, SLEEPING. The phase listener is a PhaseChanged, one method, void onPhaseChanged(int from, int to).

VariantContext, what tick is handed
MethodAnswers
Zombie zombie()the zombie carrying the variant
ServerLevel level()the dimension it stands in
LivingEntity target()the prey the mod picked, null until there is one
boolean ready()whether the variant's own delay has run out
void resetCooldown()restarts that delay once it has acted

Special variants

The eight shipped variants come in through the same door as an addon's own: the roll, the save and the tick all read SpecialVariantRegistry and cannot tell whose variant they are looking at. SpecialVariant.of(id, kind, weight, behavior) is the short way in, available from phase zero at a fixed weight; the record's full constructor accepts a live-read unlock phase and weight instead, for a variant whose availability follows a config option rather than a constant.

A namespaced id ("mymod:frostbite") keeps two mods from colliding; the eight shipped ones are bare words because saves already hold them that way. Register from onLethalBreedReady() and never unregister: a variant that vanished while a world holds zombies carrying its id would turn those zombies into a save entry that resolves to nothing. Renaming one keeps the old id readable through SpecialVariantRegistry.alias(oldId, id).

The name a player sees when specialShowName is on comes from a lang file, and the key follows from the id: the colon becomes a dot, all lowercase, behind lethalbreed.special.. A mymod:frostbite variant is therefore named in lethalbreed.special.mymod.frostbite, written in the addon's own lang file. Without that key, the key itself is what a player reads above the zombie.

VariantBehavior, one method per kind
MethodCalled
onSpawn(Zombie)PASSIVE, once, as the zombie finishes spawning
onUnassign(Zombie)when the variant is taken back off, to undo onSpawn
tick(VariantContext)ACTIVE, on the zombie's own staggered activation
onDeath(Zombie, ServerLevel)DEATH, while the zombie is still in the world

All four default to doing nothing, so an addon overrides the one its Kind calls for and leaves the rest alone. One VariantBehavior instance is shared by every zombie carrying that variant: per-zombie state belongs on the zombie itself, through a Fabric data attachment of the addon's own, not on a field of the behavior.

onUnassign covers a variant taken off a zombie, not an addon taken out of the folder. A jar pulled from mods/ calls nothing: whatever onSpawn made permanent stays in the NBT of the zombies already born, and the mod loads them without a word because it is an ordinary effect. Applying an effect with no duration is therefore a decision that outlives the addon, and an addon that wants to be able to leave cleanly applies finite durations it renews in tick.

Where to report what breaks

The repository issues, github.com/Dreyka-Oas/lethalbreed-support/issues, with a GitHub account. The mod is under the MIT license, detailed on Installation: writing an addon against the surface above needs no permission beyond it, that is what it is for.