For addon authors
Vortex Dread is written to be built on. No mixin is needed, no fork either, and
nothing has to be declared in your fabric.mod.json beyond a dependency on
the mod. Put the jar on your classpath, call the entry class, and that is the whole setup. This
page describes every public thing there is: four types, one call surface, two events. An addon
compiling against anything else is compiling against internals, and internals move.
The public surface
Everything stable lives in oas.dreyka.vortexdread.api and
its subpackage oas.dreyka.vortexdread.api.event. Two enums from
elsewhere are part of the contract because the signatures hand them to you:
oas.dreyka.vortexdread.wind.EfScale, which carries the six ratings, and
oas.dreyka.vortexdread.tornado.TornadoStage, which carries the five
stages. Those two will not move either. The rest of the tree changes from version to version and
that does not count as a regression.
The mod publishes under group oas.dreyka.vortexdread,
artefact vortexdread, version 1.0.0. No remote
maven repository is open yet: a ./gradlew publishToMavenLocal from a
clone puts the artefact in your ~/.m2, and depending on the jar from a
GitHub release works just as well. The surface described here has existed since the first
published version.
Wiring it into your build
The whole thing, since the lines that trip an addon up are the ones nobody writes
out. Loom is a Gradle plugin and it does not live on the plugin portal, so
settings.gradle.kts has to name Fabric's own maven before
build.gradle.kts can ask for it:
pluginManagement {
repositories {
maven("https://maven.fabricmc.net/") { name = "Fabric" }
gradlePluginPortal()
mavenCentral()
}
}
rootProject.name = "your-addon"
Then the plugin itself, at the top of build.gradle.kts.
The version below is the one this mod is built with, and Java 21 is what 1.21.11 runs on:
plugins {
id("fabric-loom") version "1.17.12"
java
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
}
Then the dependencies, in Kotlin DSL:
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"))
}
The same three blocks in Groovy, since a build file written in one form leaves the other form's author guessing:
// 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')
}
Use modImplementation, never
implementation or compileOnly. The
published jar carries intermediary names for every Minecraft type it touches, and only a
mod-aware configuration asks Loom to remap them into your own. Get this wrong and the compiler
reports things like cannot access class_2960 or
Identifier cannot be converted to class_1937, which look like a broken
API and are a missing three letters.
Fabric API belongs on the same configuration for the same kind of reason. The two
callbacks below are Fabric Event objects, so your code touches that
class at compile time even if it never calls Fabric API itself, and Loom only passes Fabric API's
access widener to a project that declares the module. On
modRuntimeOnly you get a missing
net.fabricmc.fabric.api.event.Event and, stranger, half the vanilla
registration calls turning private.
Mappings are not a free choice either: the mod is built against official Mojang
mappings, and every signature on this page is written in them. A Yarn project sees
ServerWorld where the tables below say
ServerLevel, and the two do not meet. In
fabric.mod.json, the id to depend on is
"vortexdread".
Six vanilla types appear in the signatures below, and the tables give their short names. Where they come from, so nothing has to be hunted for:
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;
What an addon can call and listen to
Nothing registers at boot: there is no entrypoint to declare, no interface to
implement at mod level, no load order to respect. Both events go through
net.fabricmc.fabric.api.event.Event, the machinery Fabric already ships,
so you subscribe from your own onInitialize and the mod calls you when it
has something to say. A listener that throws is blamed once per class in the server log, and the
storm carries on without it.
| Entry point | What an addon does with it | Since |
|---|---|---|
| VortexDreadApi | Static call surface. Lists the tornadoes in a level, finds the nearest one to a point, samples the wind, starts one. | 1.0.0 |
| TornadoView | Read-only interface on one live tornado. Thirteen accessors, all current for as long as the storm exists. | 1.0.0 |
| event.TornadoLifeCallback | Forming, touchdown and gone. Where a warning system or a scoreboard hooks in. | 1.0.0 |
| event.BlockTakenCallback | A veto on every block the storm is about to take. What a claim mod needs. | 1.0.0 |
| wind.EfScale | The six ratings, with their gust floor in metres per second and their translation key. | 1.0.0 |
| Signature | What it answers | Side |
|---|---|---|
| tornadoes(Level) | List<TornadoView>, every loaded tornado, in no particular order. | both |
| nearest(Level, Vec3) | Optional<TornadoView>, the one whose axis is closest to the point. | both |
| windAt(Level, Vec3) | Vec3, wind in blocks per second, summed over every storm that reaches the point. The zero vector anywhere else. | both |
| spawn(ServerLevel, double, double, EfScale) | TornadoView on the tornado made at the given x and z. The rating is a ceiling, not a promise. | server |
| Signature | What it answers | Unit |
|---|---|---|
| position() | Where the axis meets the ground. | Vec3 |
| alive() | False the moment the storm is over, and nothing moves after that. | boolean |
| rating() | The rating read off the current gust, which climbs and falls again. | EfScale |
| peakRating() | The strongest rating it has reached. What a warning should be written against. | EfScale |
| stage() | Where it is: FORMING, TOUCHDOWN, MATURE, ROPING, GONE. | TornadoStage |
| ageTicks() | Ticks since it formed. | int |
| wind() | Tangential wind at the radius of maximum wind. | float, m/s |
| coreRadius() | Radius of maximum wind. The visible condensation is a little narrower. | float, blocks |
| funnelHeight() | Ground to cloud base under the axis. | float, blocks |
| descent() | 0 hanging out of the cloud, 1 on the ground. | float |
| groundLoad() | 0 clean, 1 wrapped in what it took. | float |
| tint() | The colour the debris has given it, packed as 0xRRGGBB. | int |
| influenceBox() | The box the wind reaches into. Outside it the storm does nothing at all. | AABB |
A whole addon
This one tells the players when a funnel forms and when it is over, and refuses the
storm anything above y 200. It is one file and it compiles as it stands, given
fabric-api and the Vortex Dread jar on the 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, "Funnel forming, " + tornado.peakRating() + " expected");
}
@Override
public void onTouchdown(ServerLevel level, TornadoView tornado) {
announce(level, "On the ground, core " + (int) tornado.coreRadius() + " blocks");
}
@Override
public void onGone(ServerLevel level, TornadoView tornado) {
announce(level, "All clear");
}
});
// Nothing comes out of the ground above y 200, whatever the storm is worth.
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)));
}
}
Both onTouchdown and onGone
have an empty default body, so a class that only wants the birth writes only
onFormed. BlockTakenCallback has one method and
therefore takes a lambda. Answering false leaves the block where it is, and no other listener can
undo that. The view handed to either event is live: reading it later in the same tick is fine,
holding it past the death is not, since it answers false to alive() from
then on.
Four small guarantees that save an addon from guessing. Nothing here ever returns
null: tornadoes gives an empty list on a level with no storm in it, and
nearest gives an empty Optional. One tornado is
one TornadoView instance for its whole life, identity comparison
included, so keying a map on it holds a tally together across a thousand callbacks. Both events
fire on the server thread, and the three reading calls answer from whichever side asks, which is
what lets a client-side overlay use the same code as a server rule. And
BlockTakenCallback is handed the tornado, the position and the block that
was there, never a cause: the storm is the cause, and an addon that wants to tell wind damage from
debris damage reads the damage type on the entity side instead.
Without writing a line of Java
A good deal of what an addon would do is settable from
config/oas/vortexdread.json, server side, forty-two options across five
groups. A map where the storm breaks nothing is one line
(breakBlocks to false). A map where only the operator decides when a
tornado arrives is naturalTornadoes to false plus the
/vortex spawn ef4 command. Values are clamped rather than refused, so an
absurd number gives a playable game and a line in the log.
Everything else is internal. The packages
storm, tornado (other than
TornadoStage), wind (other than
EfScale), damage,
debris, entity,
compute, client,
config and mixin carry no stability promise.
Calling into them works today and breaks one version later, and that will not be treated as a
regression or fixed for you. If the public surface is missing something you need, ask for it on
the Issues page instead of working around it:
github.com/Dreyka-Oas/VortexDread/issues.
Licence. Vortex Dread is MIT. Take it, change it, redistribute it and publish a commercial addon on it without asking anyone. The one obligation is keeping the copyright notice and the licence text with any substantial copy of the code. A mention in your addon's description is welcome as a courtesy and never as a condition. The licence text