LethalBreed manuel de siège API pour greffons Manuel English

API pour extensions

Un autre mod atteint LethalBreed par un seul point d'entrée et une poignée de petites classes et paquets, tous sous oas.dreyka.lethalbreed.api. Rien ici n'exige de mixin : une extension déclare des options, écoute des événements, lit ce que le mod croit savoir à l'instant, et ajoute ses propres zombies spéciaux, par des appels qui restent stables d'une version à l'autre, comme le javadoc de ce package le promet déjà.

La surface, et le rôle de chaque classe
ClasseSert à
LethalBreedAddonle point d'entrée, appelé deux fois au démarrage
LethalBreedApidéclarer un namespace d'IA, écouter un changement de phase
LethalBreedConfigApienregistrer ses propres options dans le même fichier de config
LethalBreedStatelecture seule : phase, nombre suivi, variant d'un zombie, meutes
api.event.*six événements Fabric : adoption d'un zombie, contamination, humeur, entrée en meute, épuration au spawn, ciblage
api.variant.*ajouter un zombie spécial, passif, actif ou à la mort

Dépendre du mod

Il n'y a pas de dépôt Maven à viser : le jar publié sur Modrinth ou CurseForge est aussi celui contre lequel une extension se compile. Le déposer dans un dossier du projet et le déclarer en modImplementation, jamais en compileOnly ni en implementation, sans quoi Loom le laisse non remappé et le compilateur se met à parler de class_1799 plutôt que des classes qu'on attendait. L'API Fabric va sur la même configuration, parce que les événements du mod sont des Event de Fabric et qu'il en faut un au moment de compiler.

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"))
}

En Groovy le bloc se translittère à l'identique, à une règle de Gradle près : la version d'un greffon y doit rester un littéral, donc id 'fabric-loom' version '1.17.12' et jamais une propriété. Le modid à mettre dans le depends de son fabric.mod.json est lethalbreed.

Le point d'entrée

LethalBreedAddon est une interface fonctionnelle avec une méthode obligatoire et une facultative, appelées à deux moments distincts du démarrage. Déclarer la classe qui l'implémente sous le point d'entrée lethalbreed:addon de son propre fabric.mod.json :

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

onLethalBreedReady() tourne avant que le fichier de config soit lu et avant que quoi que ce soit lise un registre : c'est là que va LethalBreedConfigApi.register, avec tout namespace d'IA autorisé et tout écouteur de phase, assez tôt pour être connu avant le premier zombie chargé. onConfigLoaded() est une méthode par défaut vide, appelée une fois le fichier réellement lu et borné ; ne la surcharger que si une de ses décisions dépend de la valeur réelle d'une option plutôt que de son défaut.

Une extension complète

Tout ce qui suit compile contre le package tel qu'il est : un namespace autorisé, une option enregistrée à côté de celles du mod, un écouteur de phase, un variant spécial, et un écouteur d'événement qui épargne un boss de l'épuration au spawn du mod.

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;
}

Chaque champ de la classe support doit commencer par le préfixe revendiqué, vérifié plutôt que présumé : un champ qui l'aurait raté atterrirait sinon dans l'onglet Misc à côté de l'option d'un inconnu. Le préfixe lui-même fait au moins trois caractères, et en prendre un déjà pris, ou un qui capturerait une option déjà existante, lève une IllegalArgumentException avant que quoi que ce soit ne soit écrit.

Le dernier paramètre de register est variadique : on passe autant d'OptionBounds qu'on veut, un par option à borner, et une option qu'on n'y nomme pas passe sans être bornée. L'écran de configuration étiquette une ligne avec lethalbreed.option.<nomDuChamp> et un onglet avec lethalbreed.category.<categorie>, le nom brut servant de repli. La catégorie y entre telle qu'elle a été passée, casse comprise : les onglets livrés s'écrivent lethalbreed.category.Perf ou lethalbreed.category.Compute, donc un register("mymod", "MyMod", ...) se nomme dans lethalbreed.category.MyMod, jamais sous le préfixe. Ces deux clés s'écrivent dans le fichier de langue de l'extension, sous le préfixe lethalbreed. : le jeu fusionne les fichiers de langue entre mods, donc rien à toucher du côté de celui-ci.

Lire ce que le mod sait

LethalBreedState est la moitié lecture seule de la surface : rien ici ne change quoi que ce soit, et rien n'exige d'écouteur enregistré au préalable. Tout ce qui traverse la frontière est un type vanilla, un primitif, ou LethalBreedState.Pack, si bien qu'une extension compilée contre elle continue de compiler quand les classes d'état interne du mod changent de forme. Thread serveur uniquement, comme le reste du mod.

lire l'étatint phase = LethalBreedState.phase();
int tracked = LethalBreedState.trackedZombieCount();
SpecialVariant variant = LethalBreedState.variantOf(zombie);
List<LethalBreedState.Pack> packs = LethalBreedState.packs(level);
LethalBreedState, méthode par méthode
MéthodeRépond
phase()la progression de la nuit, zéro sur un monde neuf, sans plafond
trackedZombieCount()les zombies pilotés par le mod, toutes dimensions confondues
variantOf(Zombie)le variant spécial qu'il porte, ou null pour un zombie ordinaire
packs(ServerLevel)chaque meute de cette dimension, en instantané pris à l'appel

Événements

Six événements Fabric, chacun avec une seule question et une réponse chaînée : un écouteur reçoit le verdict tel qu'il se tient et renvoie celui qu'il veut, si bien qu'un écouteur sans avis sur un cas donné renvoie proposed et ne change rien. Un écouteur qui lève une exception est signalé une fois, dans le journal serveur, puis traité comme s'il s'était abstenu ; le mod ne s'arrête pas pour une extension cassée.

Les six événements, et ce que chacun décide
ÉvénementMéthodeDécide
ContaminationCallbackboolean allowContamination(LivingEntity, boolean)si une créature attrape la contamination
MoodCallbackMoodState mood(Zombie, MoodState)laquelle des cinq humeurs un zombie tient pour une activation
PackJoinCallbackboolean allowJoin(Zombie, long, boolean)si un zombie est admis dans une meute, id de meute 0 (PackJoinCallback.FOUNDING) signifie une fondation plutôt qu'une adhésion
SpawnCullCallbackboolean allowCull(Entity, int, boolean)si un hostile est épuré (retiré) ; renvoyer true pour l'épurer, false pour qu'il survive
TargetCandidateCallbackboolean isValidPrey(Mob, LivingEntity, boolean)si une créature est une proie pour un zombie
ZombieAdoptCallbackboolean allowAdopt(Zombie, boolean)si un zombie est repris par le cerveau du mod ; renvoyer false le laisse entièrement au jeu de base

ZombieAdoptCallback est la sortie de secours d'une extension qui a son propre zombie. Tout ce qui charge en étant un Zombie est repris : husk, noyé, villageois zombie, et la sous-classe qu'un autre mod aura écrite. C'est le bon défaut, puisque la horde entière est ce à quoi le mod sert, mais une créature écrite pour faire autre chose se retrouve les goals retirés, la navigation pilotée par le flow field et une meute qui l'emmène ailleurs. Refuser l'adoption est total : le zombie garde son IA, n'entre dans aucune meute, n'est jamais activé. Il reste un hostile dans un monde où le mod épure les hostiles, donc ce refus s'accompagne presque toujours d'un SpawnCullCallback. La question est posée à chaque chargement et pas une seule fois dans la vie du monde, parce que l'extension qui l'a revendiqué peut fort bien avoir été absente la fois précédente.

TargetCandidateCallback tourne sur la phase large. Plusieurs centaines de fois par tick sur un serveur chargé, dans le balayage qui chiffre déjà le coût d'une horde. Lire un attachment, comparer un type d'entité, renvoyer : tout ce qui alloue, parcourt un chunk ou prend un verrou va ailleurs. Un datapack qui veut seulement épargner une liste fixe de types d'entités de l'épuration au spawn veut plutôt le tag lethalbreed:spawn_protected, sans une ligne de code.

Les types qui traversent la frontière

Les signatures ci-dessus nomment leurs types sans leur paquet, et deviner celui d'un type du jeu coûte une compilation : Zombie a déménagé en 1.21 et vit maintenant sous monster.zombie. Les imports exacts, côté mod et côté jeu, pour les six événements, les variants et la lecture d'état :

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;

Les sous-classes ont suivi Zombie dans le même dossier : Husk, Drowned et ZombieVillager vivent sous net.minecraft.world.entity.monster.zombie elles aussi, ce qui compte pour une extension qui trie par type dans un écouteur d'adoption.

MoodState est une énumération de cinq constantes, NORMAL, FLEEING, SHELTERING, CELEBRATING, SLEEPING. L'écouteur de phase est un PhaseChanged, une seule méthode void onPhaseChanged(int from, int to).

VariantContext, ce que reçoit tick
MéthodeRépond
Zombie zombie()le zombie qui porte le variant
ServerLevel level()la dimension où il se tient
LivingEntity target()la proie choisie par le mod, null tant qu'il n'y en a pas
boolean ready()si le délai propre au variant est écoulé
void resetCooldown()relance ce délai après avoir agi

Variants spéciaux

Les huit variants livrés entrent par la même porte qu'un variant d'extension : le tirage, la sauvegarde et le tick lisent tous SpecialVariantRegistry et ne savent pas distinguer un variant de l'autre. SpecialVariant.of(id, kind, weight, behavior) est le chemin court, disponible dès la phase zéro à un poids fixe ; le constructeur complet du record accepte à la place une phase de déblocage et un poids lus en direct, pour un variant dont la disponibilité suit une option de config plutôt qu'une constante.

Un id namespacé ("mymod:frostbite") évite la collision entre deux mods ; les huit livrés sont des mots nus parce que les sauvegardes les tiennent déjà ainsi. Enregistrer depuis onLethalBreedReady() et ne jamais désenregistrer : un variant disparu alors qu'un monde tient encore des zombies portant son id transformerait ces zombies en entrée de sauvegarde qui ne résout plus vers rien. Renommer un variant garde l'ancien id lisible via SpecialVariantRegistry.alias(oldId, id).

Le nom qu'un joueur voit quand specialShowName est actif se lit dans un fichier de langue, et la clé se déduit de l'id : le deux-points devient un point, le tout en minuscules, derrière lethalbreed.special.. Un variant mymod:frostbite se nomme donc dans lethalbreed.special.mymod.frostbite, écrit dans le fichier de langue de l'extension. Sans cette clé, c'est elle que le joueur lit au-dessus du zombie.

VariantBehavior, une méthode par nature
MéthodeAppelée
onSpawn(Zombie)PASSIVE, une fois, quand le zombie finit son spawn
onUnassign(Zombie)quand le variant est retiré, pour défaire onSpawn
tick(VariantContext)ACTIVE, sur l'activation propre au zombie, décalée
onDeath(Zombie, ServerLevel)DEATH, pendant que le zombie est encore dans le monde

Les quatre méthodes ne font rien par défaut, si bien qu'une extension ne surcharge que celle qu'appelle son Kind et laisse les autres telles quelles. Une seule instance de VariantBehavior est partagée par tous les zombies portant ce variant : l'état propre à un zombie va sur le zombie lui-même, via un data attachment Fabric propre à l'extension, pas sur un champ du behavior.

onUnassign couvre le variant qu'on retire d'un zombie, pas l'extension qu'on retire du dossier. Un jar sorti de mods/ n'appelle plus rien : ce que onSpawn a posé de permanent reste dans la NBT des zombies déjà nés, et le mod les charge sans rien dire puisque c'est un effet ordinaire. Poser un effet sans durée est donc une décision qui survit à l'extension, et une extension qui veut pouvoir disparaître proprement pose des durées finies qu'elle renouvelle dans tick.

Où signaler ce qui casse

Le carnet de rapports du dépôt, github.com/Dreyka-Oas/lethalbreed-support/issues, avec un compte GitHub. Le mod est sous licence MIT, détaillée sur Installation : écrire une extension contre la surface ci-dessus ne demande aucune permission au-delà, c'est précisément à cela qu'elle sert.