Pour les auteurs d'addons

Vortex Dread est écrit pour qu'on greffe des choses dessus. Aucun mixin n'est nécessaire, aucun fork non plus, et rien n'a besoin d'être déclaré dans votre fabric.mod.json au-delà d'une dépendance sur le mod. Vous ajoutez le jar à votre classpath, vous appelez la classe d'entrée, et c'est tout. Cette page décrit l'intégralité de ce qui est public: quatre types, une classe d'appel, deux événements. Un addon qui compile contre autre chose que ça compile contre de l'interne, et l'interne bouge.

La surface publique

Tout ce qui est stable vit dans oas.dreyka.vortexdread.api et dans son sous-paquet oas.dreyka.vortexdread.api.event. Deux types énumérés venus d'ailleurs entrent dans le contrat parce que les signatures les exposent: oas.dreyka.vortexdread.wind.EfScale, qui porte les six cotes, et oas.dreyka.vortexdread.tornado.TornadoStage, qui porte les cinq stades. Ces deux-là ne bougeront pas non plus. Le reste de l'arborescence change au gré des versions sans que ça compte comme une régression.

Le mod se publie sous le groupe oas.dreyka.vortexdread, l'artefact vortexdread, version 1.0.0. Aucun dépôt maven distant n'est ouvert pour l'instant: un ./gradlew publishToMavenLocal depuis une copie du dépôt met l'artefact dans votre ~/.m2, et une dépendance sur le jar d'une release GitHub marche tout aussi bien. La surface décrite ici existe depuis la première version publiée.

Le brancher dans votre build

Tout le montage, parce que les lignes qui coincent sont celles que personne n'écrit. Loom est un plugin Gradle et il n'habite pas sur le portail des plugins, donc settings.gradle.kts doit nommer le maven de Fabric avant que build.gradle.kts puisse le réclamer:

pluginManagement {
    repositories {
        maven("https://maven.fabricmc.net/") { name = "Fabric" }
        gradlePluginPortal()
        mavenCentral()
    }
}

rootProject.name = "votre-addon"

Puis le plugin lui-même, en tête de build.gradle.kts. La version ci-dessous est celle avec laquelle le mod est compilé, et Java 21 est ce sur quoi tourne 1.21.11:

plugins {
    id("fabric-loom") version "1.17.12"
    java
}

java {
    toolchain.languageVersion = JavaLanguageVersion.of(21)
}

Puis les dépendances, en DSL Kotlin:

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

    modImplementation(files("libs/vortexdread-1.0.0.jar"))
}

Les trois mêmes blocs en Groovy, parce qu'un fichier de build écrit dans une seule des deux formes laisse l'auteur de l'autre deviner:

// settings.gradle
pluginManagement {
    repositories {
        maven { url = 'https://maven.fabricmc.net/'; name = 'Fabric' }
        gradlePluginPortal()
        mavenCentral()
    }
}

// build.gradle
plugins {
    id 'fabric-loom' version '1.17.12'
    id 'java'
}

dependencies {
    minecraft 'com.mojang:minecraft:1.21.11'
    mappings loom.officialMojangMappings()
    modImplementation 'net.fabricmc:fabric-loader:0.19.3'
    modImplementation 'net.fabricmc.fabric-api:fabric-api:0.141.4+1.21.11'

    modImplementation files('libs/vortexdread-1.0.0.jar')
}

Prenez modImplementation, jamais implementation ni compileOnly. Le jar publié porte des noms intermédiaires pour chaque type de Minecraft qu'il touche, et seule une configuration qui sait ce qu'est un mod demande à Loom de les retraduire vers les vôtres. En cas d'erreur, le compilateur sort des cannot access class_2960 ou des Identifier cannot be converted to class_1937, qui ressemblent à une API cassée et sont trois lettres manquantes.

Fabric API se déclare sur la même configuration, pour une raison du même ordre. Les deux rappels décrits plus bas sont des Event de Fabric, donc votre code touche cette classe à la compilation même s'il n'appelle jamais Fabric API lui-même, et Loom ne transmet l'access widener de Fabric API qu'à un projet qui déclare le module. Sur modRuntimeOnly, vous récupérez un net.fabricmc.fabric.api.event.Event introuvable et, plus surprenant, la moitié des appels d'enregistrement de vanilla qui passent en privé.

Les mappings ne sont pas libres non plus: le mod est compilé contre les mappings officiels de Mojang, et toutes les signatures de cette page sont écrites dedans. Un projet sous Yarn voit ServerWorld là où les tableaux ci-dessous disent ServerLevel, et les deux ne se rencontrent pas. Dans fabric.mod.json, l'identifiant dont dépendre est "vortexdread".

Six types de vanilla apparaissent dans les signatures ci-dessous, et les tableaux n'en donnent que le nom court. D'où ils viennent, pour n'avoir rien à chercher:

import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.level.Level;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;

import oas.dreyka.vortexdread.api.VortexDreadApi;
import oas.dreyka.vortexdread.api.TornadoView;
import oas.dreyka.vortexdread.api.event.TornadoLifeCallback;
import oas.dreyka.vortexdread.api.event.BlockTakenCallback;
import oas.dreyka.vortexdread.wind.EfScale;
import oas.dreyka.vortexdread.tornado.TornadoStage;

Ce qu'un addon peut appeler et écouter

Rien ne s'enregistre au démarrage: il n'y a pas d'entrypoint à déclarer, pas d'interface à implémenter au niveau du mod, pas d'ordre de chargement à respecter. Les deux événements passent par net.fabricmc.fabric.api.event.Event, la mécanique que Fabric fournit déjà, donc vous vous abonnez depuis votre propre onInitialize et le mod vous appelle quand il a quelque chose à dire. Un écouteur qui lève une exception est signalé une fois par classe, dans le journal du serveur, et la tempête continue sans lui.

Les cinq points d'entrée, tous dans oas.dreyka.vortexdread.api
Point d'entréeCe qu'un addon en faitDepuis
VortexDreadApiClasse statique. Liste les tornades d'un monde, trouve la plus proche d'un point, échantillonne le vent, en fait naître une.1.0.0
TornadoViewInterface en lecture seule sur une tornade vivante. Treize accesseurs, tous à jour tant que la tempête existe.1.0.0
event.TornadoLifeCallbackNaissance, contact et fin. C'est là qu'un système d'alerte ou un tableau des scores se branche.1.0.0
event.BlockTakenCallbackVeto sur chaque bloc que la tempête s'apprête à emporter. Ce qu'il faut à un mod de protection de zone.1.0.0
wind.EfScaleLes six cotes, avec leur plancher de rafale en mètres par seconde et leur clé de traduction.1.0.0
Les méthodes de VortexDreadApi, toutes statiques
SignatureCe qu'elle rendCôté
tornadoes(Level)List<TornadoView>, toutes les tornades chargées, sans ordre garanti.les deux
nearest(Level, Vec3)Optional<TornadoView>, celle dont l'axe est le plus près du point.les deux
windAt(Level, Vec3)Vec3, le vent en blocs par seconde, somme de toutes les tempêtes qui atteignent ce point. Vecteur nul ailleurs.les deux
spawn(ServerLevel, double, double, EfScale)TornadoView sur la tornade créée aux coordonnées x et z données. La cote est un plafond, pas une promesse.serveur
Les accesseurs de TornadoView
SignatureCe qu'elle rendUnité
position()Le point où l'axe rencontre le sol.Vec3
alive()Faux dès que la tempête est finie, et plus rien ne bouge après.boolean
rating()La cote lue sur la rafale actuelle, qui monte puis redescend.EfScale
peakRating()La plus forte cote atteinte. C'est contre celle-là qu'une alerte s'écrit.EfScale
stage()Où elle en est: FORMING, TOUCHDOWN, MATURE, ROPING, GONE.TornadoStage
ageTicks()Ticks écoulés depuis la formation.int
wind()Vent tangentiel au rayon de vent maximal.float, m/s
coreRadius()Rayon de vent maximal. La condensation visible est un peu plus étroite.float, blocs
funnelHeight()Du sol à la base nuageuse, sous l'axe.float, blocs
descent()0 quand l'entonnoir pend hors du nuage, 1 quand il est au sol.float
groundLoad()0 propre, 1 enveloppée de ce qu'elle a pris.float
tint()Couleur que les débris lui ont donnée, empaquetée en 0xRRGGBB.int
influenceBox()La boîte où le vent porte. Dehors, la tempête ne fait rien du tout.AABB

Un addon complet

Celui-ci prévient les joueurs quand un entonnoir se forme et quand il est fini, et il interdit à la tempête de toucher quoi que ce soit au-dessus de l'altitude 200. C'est un fichier et il compile tel quel, à condition d'avoir fabric-api et le jar de Vortex Dread sur le classpath.

package example.stormalarm;

import net.fabricmc.api.ModInitializer;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import oas.dreyka.vortexdread.api.TornadoView;
import oas.dreyka.vortexdread.api.event.BlockTakenCallback;
import oas.dreyka.vortexdread.api.event.TornadoLifeCallback;

public class StormAlarm implements ModInitializer {

    @Override
    public void onInitialize() {
        TornadoLifeCallback.EVENT.register(new TornadoLifeCallback() {

            @Override
            public void onFormed(ServerLevel level, TornadoView tornado) {
                announce(level, "Entonnoir en formation, " + tornado.peakRating() + " attendue");
            }

            @Override
            public void onTouchdown(ServerLevel level, TornadoView tornado) {
                announce(level, "Contact au sol, coeur de " + (int) tornado.coreRadius() + " blocs");
            }

            @Override
            public void onGone(ServerLevel level, TornadoView tornado) {
                announce(level, "Fin d'alerte");
            }
        });

        // Rien ne sort du sol au-dessus de 200, quelle que soit la force de la tempete.
        BlockTakenCallback.EVENT.register((level, tornado, pos, state) -> pos.getY() < 200);
    }

    private static void announce(ServerLevel level, String line) {
        level.players().forEach(player -> player.sendSystemMessage(Component.literal(line)));
    }
}

Les deux méthodes onTouchdown et onGone ont un corps par défaut vide, donc une classe qui ne veut que la naissance n'écrit que onFormed. BlockTakenCallback n'a qu'une méthode et accepte donc une lambda. Rendre faux laisse le bloc en place, et aucun autre écouteur ne peut revenir dessus. La vue passée aux deux événements est vivante: la lire dans le même tick va, la garder après la mort de la tempête ne va pas, puisqu'elle répond faux à alive() à partir de là.

Quatre petites garanties qui évitent à un addon de deviner. Rien ne renvoie jamais null: tornadoes rend une liste vide sur un niveau sans tempête, et nearest rend un Optional vide. Une tornade est une seule instance de TornadoView pendant toute sa vie, comparaison d'identité comprise, donc un compteur rangé dans une map reste d'un seul morceau à travers mille rappels. Les deux événements partent sur le fil du serveur, et les trois appels de lecture répondent du côté qui demande, ce qui permet à une surcouche cliente d'utiliser le même code qu'une règle serveur. Enfin, BlockTakenCallback reçoit la tornade, la position et le bloc qui était là, jamais une cause: la cause est la tempête, et un addon qui veut distinguer les dégâts du vent de ceux des débris lit le type de dégât du côté des entités.

Sans écrire une ligne de Java

Une grande partie de ce qu'un addon ferait se règle depuis config/oas/vortexdread.json, côté serveur, quarante-deux options réparties en cinq groupes. Une carte où la tempête ne casse rien, ça tient en une ligne (breakBlocks à faux). Une carte où seul l'opérateur décide quand une tornade arrive, c'est naturalTornadoes à faux plus la commande /vortex spawn ef4. Les valeurs sont bornées et corrigées plutôt que refusées, donc un chiffre absurde donne une partie jouable et une ligne dans le journal.

Tout le reste est de l'interne. Les paquets storm, tornado (hors TornadoStage), wind (hors EfScale), damage, debris, entity, compute, client, config et mixin n'ont aucune promesse de stabilité. Appeler ce qui s'y trouve marche aujourd'hui et cassera à une version près, sans que ce soit traité comme une régression ni corrigé pour vous. Si quelque chose vous manque dans la surface publique, demandez-le sur la page Issues plutôt que de contourner: github.com/Dreyka-Oas/VortexDread/issues.

Licence. Vortex Dread est sous licence MIT. Vous pouvez le reprendre, le modifier, le redistribuer et publier un addon commercial dessus sans rien demander. La seule obligation est de conserver l'avis de copyright et le texte de la licence avec toute copie substantielle du code. Une mention dans la description de votre addon fait plaisir et n'est jamais une condition. Le texte de la licence