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

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.

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.

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).

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/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.