Addons entwickeln
Addons erweitern den Client um HUD-Elemente, Einstellungen und – wenn nötig – Eingriffe direkt in Minecraft. Sie werden in Java geschrieben und im Spiel mit einem Klick installiert.
Überblick
Ein Addon ist ein Jar mit drei Bestandteilen:
- Eine Klasse, die
client.api.Moduleimplementiert – liefert HUD-Elemente und Einstellungen. - Eine
module.json– Name, Version, Beschreibung und optional Patches. - Eine Service-Datei
META-INF/services/client.api.Module– sagt dem Client, welche Klasse er laden soll.
Der Client lädt jedes Addon in einem eigenen ClassLoader. Du hast Zugriff auf die komplette Minecraft-API (Minecraft 26.3, unverschleierte Namen) und auf das kleine client.api-Paket.
net.minecraft.client.Minecraft.Schnellstart
Als Beispiel eine Uhr oben rechts im HUD, mit einem Schalter für Sekunden. Lege einen Ordner Client/Addons/uhr an:
Client/Addons/uhr/src/uhr/Uhr.javapackage uhr;
import client.api.AddonConfig;
import client.api.HudColor;
import client.api.HudElement;
import client.api.Module;
import client.api.Setting;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import net.minecraft.client.Minecraft;
public final class Uhr implements Module {
private final AddonConfig cfg = AddonConfig.of("uhr");
private boolean seconds = cfg.bool("seconds", false);
// Oben rechts: Anker x = 1 (rechter Rand), 54 Pixel nach links versetzt, 4 nach unten
@Override
public List<HudElement> hud() {
return List.of(new HudElement("uhr", "Uhr", 1, 0, -54, 4, 50, 12, (g, x, y) -> {
String text = LocalTime.now().format(DateTimeFormatter.ofPattern(seconds ? "HH:mm:ss" : "HH:mm"));
g.text(Minecraft.getInstance().font, text, x, y + 2, HudColor.at(x, y), true);
}));
}
// Erscheint unter Einstellungen → Addons → Uhr
@Override
public List<Setting> options() {
return List.of(
Setting.heading("Anzeige"),
Setting.toggle("Sekunden anzeigen", "Uhrzeit als HH:mm:ss",
() -> seconds, v -> cfg.set("seconds", seconds = v)));
}
}
Client/Addons/uhr/resources/META-INF/services/client.api.Moduleuhr.Uhr
Client/Addons/uhr/module.json{
"name": "Uhr",
"developer": "Dein Name",
"version": "1.0.0",
"minecraft": "26.3",
"description": "Zeigt die Uhrzeit im HUD – mit oder ohne Sekunden."
}
Bauen und den Addon-Server starten:
cd Launcher
npm run build:client # baut Client und alle Addons, prüft Patches
npm run server # Addon-Server auf http://localhost:8787
Im Spiel erscheint „Uhr“ jetzt unter Addons. Nach dem Installieren ist die Uhr sofort im HUD – verschieben und skalieren im HUD-Editor (Rechte Shift-Taste).
Eigenes Projekt (Gradle)
Du entwickelst außerhalb dieses Repos? Lade die Projektvorlage (Gradle, mit Beispiel-Addon). Sie bindet die Modul-API als Bibliothek ein, lädt Minecraft einmalig von Mojang und erzeugt die Service-Datei selbst.
gradlew build # → build/libs/<id>.jar, prüft Patches
gradlew runClient # Spiel mit Client + Addon starten (offline als „Dev“, Ordner run/)
gradlew runClient --debug-jvm # dasselbe, wartet auf einen Debugger an Port 5005
gradlew runClient -Pminecraft=26.3 # auf einer bestimmten Minecraft-Version (bauen/testen genauso)
gradlew minecraftVersions # Minecraft-Versionen, die der Client unterstützt
gradlew install # neues Jar ins laufende Spiel, dann im Chat: .addon reload <id>
gradlew runTest # lokaler Vanilla-Server + Spiel, Addon prüft sich selbst (client.api.Testing)
gradlew publishAddon -PaddonToken=… # zur Freigabe an den Addon-Server
- Die ID steht in
settings.gradle(rootProject.name), Klasse und API-Version insrc/main/resources/module.json(main,api). - Die API kommt als Maven-Artefakt
client:client-api:<api>vom Addon-Server (/maven). Andere Adresse:-PclientServer=https://…oderclientServer=…ingradle.properties. - Mit Quellen und Javadoc: IDEs (IntelliJ, VS Code) zeigen Doku und Code der API direkt an – oder online in der API-Referenz. Unter Linux/macOS vorher
chmod +x gradlew.
build.gradle (Ausschnitt)repositories { maven { url = 'https://<addon-server>/maven' } }
dependencies {
compileOnly "client:client-api:6.2.1"
compileOnly files(minecraft) // 26.3.jar + Libraries, lädt die Vorlage selbst
}
Projektaufbau
Client/Addons/<id>/
├─ module.json Name, Version, Beschreibung, Patches
├─ src/<paket>/…java Quellcode
├─ resources/ landet unverändert im Jar
│ └─ META-INF/services/client.api.Module
└─ test/…java optional: Selbsttests, laufen bei jedem Build
- Die ID ist der Ordnername: nur
a–z,0–9und-. Daraus werden<id>.jarund die Speicherdatei<id>.properties. - Kompiliert wird mit
--release 25gegen Minecraft 26.3 und den Client. Ein JDK ab Version 25 muss installiert sein. - Das fertige Jar samt
module.jsonliegt danach inServer/Backend/modules/<id>/.
module.json
| Feld | Pflicht | Bedeutung |
|---|---|---|
name | ja | Anzeigename im Katalog und als Reiter unter Einstellungen → Addons. |
developer | ja | Dein Name oder Team. |
version | ja | Bei jeder Änderung erhöhen – nur so sehen Spieler ein Update. |
minecraft | ja | Minecraft-Version, gegen die das Addon gebaut ist, z. B. 26.3. |
description | ja | Beschreibung für Katalog und Website. Klartext, keine Formatierung. |
api | nein | API-Version, gegen die das Addon gebaut ist, als Semver-Text (Module.VERSION, aktuell "6.2.1"; alte Zahlen wie 6 gelten als 6.0.0; fehlt = 1). Ist sie höher als die des Clients, lädt das Addon nicht. Was mit welcher Version dazukam: Changelog. |
main | nur Vorlage | Deine Module-Klasse; die Gradle-Vorlage erzeugt daraus die Service-Datei. |
category | nein | Eine von hud, pvp, minimap, visual, utility, chat, performance, other (Standard). Server können ganze Kategorien abschalten – ehrlich wählen, sonst gibt es keine Freigabe. |
permissions | nein | Was dein Addon darf, siehe Berechtigungen. Spieler sehen die Liste vor dem Installieren. |
requires | nein | IDs anderer Addons aus dem Katalog, z. B. ["minimap"]. Werden mitinstalliert und vorher geladen. Verständigt euch über den Bus. |
hooks | bei Patches | Klasse mit den Hook-Methoden, als interner Name, z. B. meinaddon/Hooks. |
patches | nein | Eingriffe in Minecraft-Methoden, siehe Patches. |
Lebenszyklus
Wann dein Code läuft, hängt davon ab, was das Addon braucht:
Die Patches aus der module.json baut der Loader ein, bevor Minecrafts Klassen laden. Greifen nach dem Installieren erst ab dem nächsten Spielstart.
Minecraft ist erstellt (Fenster, Optionen, Spielordner), Texturen und Übersetzungen laden noch.
ready()Texturen, Schriften und Übersetzungen sind da.
Wird ein Addon installiert, während das Spiel läuft, folgen Schritt 2 und 3 sofort nacheinander – ohne Neustart, solange es keine Patches mitbringt.
public interface Module {
default void ready() {} // Spiel fertig geladen
default List<HudElement> hud() { return List.of(); } // HUD-Elemente
default List<Setting> options() { return List.of(); } // Einstellungen
default List<KeyMapping> keys() { return List.of(); } // Tasten
default void unload() {} // wird entladen: Update, Deinstallation, .addon reload
// + Events, siehe unten
}
In unload() beendest du eigene Threads und schließt Dateien. Bus-Empfänger, Tasten und HUD-Elemente räumt der Client selbst ab.
Events
Für das Spielgeschehen überschreibst du einfach die passende Methode – ohne Patches, ohne Neustart. Alle laufen im Render-Thread.
default void tick() {} // 20× pro Sekunde, auch im Menü
default void join(String server) {} // Welt betreten; null = Einzelspieler
default void leave() {} // Welt/Server verlassen
default boolean chat(Component message) { return false; } // Nachricht vor der Anzeige; true = ausblenden
default boolean send(String message) { return false; } // eigene Eingabe, Befehle mit „/“; true = nicht senden
default void screen(Screen screen) {} // gezeigter Bildschirm (null = im Spiel)
default void world(float partialTick) {} // jedes Bild: in die Welt zeichnen (Gizmos)
default boolean attack(Entity target) { return false; } // eigener Angriff; true = abbrechen
default void damage(Entity entity, DamageSource source) {} // Wesen in Sichtweite wurde getroffen
default void slot(int slot) {} // Hotbar-Slot wechselt (0–8)
default void blockBroken(BlockPos pos, BlockState state) {}// Block abgebaut (state = vorher)
default void use(InteractionHand hand, BlockHitResult hit) {} // Rechtsklick; hit = null: in die Luft
default boolean sound(SoundInstance sound) { return false; } // Geräusch startet; true = stumm
default boolean title(String kind, Component text) { return false; } // "title" / "subtitle" / "actionbar"; true = ausblenden
default void death(Component message) {} // du bist gestorben
default void respawn() {} // nach dem Tod wiedergeboren (nicht beim Dimensionswechsel)
default boolean key(int key, int action, int modifiers) { return false; } // Taste (Berechtigung "input")
default boolean mouse(int button, int action, int modifiers) { return false; } // Maustaste ("input")
default boolean scroll(double amount) { return false; } // Mausrad ("input"); true = abfangen, z. B. Zoom
default void tickStart() {} // wie tick, aber bevor die Welt tickt
default void dimension(ResourceKey<Level> from, ResourceKey<Level> to) {} // Portal, Tod, /execute in
default void health(float before, float now) {} // eigene Lebenspunkte (2 = ein Herz)
default void container(AbstractContainerScreen<?> screen, AbstractContainerMenu menu) {} // Inventar, Kiste, Ofen …
default void pickup(ItemStack stack) {} // Gegenstände aufgehoben (Kopie)
default void playerJoined(String name) {} // Spieler neu in der Tab-Liste
default void playerLeft(String name) {} // Spieler aus der Tab-Liste verschwunden
default void hudHidden(boolean hidden) {} // F1: HUD aus-/eingeblendet
default void blocked(boolean blocked) {} // Server schaltet dich ab (true) / wieder an (false)
default void equipment(EquipmentSlot slot, ItemStack before, ItemStack now) {} // Rüstung/Hände: Gegenstand oder Haltbarkeit
default void xp(int level, float progress) {} // Erfahrung (progress 0…1 zur nächsten Stufe)
default void food(int before, int now) {} // Hunger 0–20
default void effect(MobEffectInstance effect, boolean added) {} // Effekt bekommen / verloren
default void screenshot(Path file, String info) {} // F2: PNG gespeichert, info = Metadaten (JSON)
default void tooltip(ItemStack stack, List<Component> lines) {} // Tooltip eines Gegenstands: Zeilen ändern/anhängen
default void itemOverlay(GuiGraphicsExtractor g, ItemStack stack, int x, int y) {} // über jeden Slot zeichnen (Inventar, Hotbar)
Eingabe abfangen (true) geht nur im Spiel – ist ein Menü offen, zählt die Rückgabe nicht, damit kein Addon Esc oder die Einstellungen blockieren kann. chat und send brauchen die Berechtigung "chat".
Berechtigungen
| Berechtigung | Bedeutet |
|---|---|
chat | Events chat und send – Chat lesen, ausblenden, Eingaben abfangen; ab 6.2.0 auch Chat.onReceive (filtern, umschreiben) |
input | Events key, mouse, scroll |
server | Net – Daten mit dem Server tauschen |
network | Web (ab 6.0.1 geprüft); eigene Verbindungen ins Internet |
files | Dateien außerhalb von client-modules/<id>/ (nur angezeigt) |
friends | Friends – Freundesliste lesen, Nachrichten empfangen und Freunden schreiben. Bekommt nur, wer es angibt (auch Addons von vor API 5). |
Ab "api": 5 bekommt ein Addon chat, input und server nur, wenn es sie angibt; ältere Addons haben weiter alles. Web prüft network ab 6.0.1; eigene Verbindungen ohne Web und files kann der Client nicht durchsetzen (Addons sind normales Java) – sie sagen Spielern und bei der Freigabe ehrlich, was das Addon tut.
In die Welt zeichnen@Override
public void world(float partialTick) {
Gizmos.cuboid(new BlockPos(0, 64, 0), GizmoStyle.stroke(0xFFFF79C6)); // Kasten um einen Block
Gizmos.billboardTextOverBlock("Zuhause", new BlockPos(0, 64, 0), 0, 0xFFFFFFFF, 0.5f);
}
world nutzt Vanillas Gizmos (net.minecraft.gizmos.Gizmos: cuboid, line, arrow, circle, billboardText…). Was du dort aufrufst, gilt genau für dieses Bild – jedes Bild neu zeichnen.
Welt-API (ab 6.2.0)@Override
public void world(float partialTick) { // jedes Bild
Render.box(new AABB(home), 0x40FF79C6, true); // gefüllter Kasten
Render.text3d(Vec3.atCenterOf(home).add(0, 1, 0), Component.literal("Zuhause"), 1, false);
Render.item3d(Vec3.atCenterOf(home).add(0, 2, 0), new ItemStack(Items.DIAMOND), true);
Render.beam(home, 0xFFFF79C6, 256);
}
@Override
public void ready() { // einmal – bleibt, bis du es entfernst
GhostBlocks.place(pos, Blocks.GLASS.defaultBlockState());
BlockTint.set(s -> s.is(Blocks.DIAMOND_ORE), 0x6000FFFF);
Sky.color(0xFF2A0A3A);
Weather.visual(Weather.Type.SNOW);
PostFx.shader(Identifier.withDefaultNamespace("invert"));
}
| Klasse | Wofür |
|---|---|
Render | Nur in world: box, line, text3d, item3d, beam |
GhostBlocks | Blöcke nur für dich (place), eigene Modelle aus Blockbench-.glb (load + model, Animation „idle“) |
Particles | Vanilla-Partikel (spawn), eigene mit Textur, Farbverlauf und Größe (Spec, custom), Quellen an Stellen oder Wesen (emitter) |
Highlight, BlockTint | Leucht-Umriss für Wesen, Rahmen um Blöcke; Blöcke im Umkreis einfärben (nur sichtbare Seiten) |
Decals | Textur auf eine Blockseite kleben |
Sky, Weather, PostFx | Himmel, Nebel, Wolken umfärben; Wetter nur optisch; Vollbild-Shader (Vanilla oder assets/<id>/post_effect/) |
Entities, Trails, Light | Wesen ausblenden, vergrößern, einfärben; Bewegungsspuren; Lichtstufen-Anzeige |
Events, Chat, Kamera (ab 6.2.0)Events.onJoin(server -> Notify.show("Willkommen", server == null ? "Einzelspieler" : server));
Chat.onReceive(m -> m.getString().contains("[Werbung]") ? null : m); // null = ausblenden
Waypoints.add("Zuhause", player.blockPosition(), 0xFFFF79C6); // sehen alle Addons
Camera.freelook(freelookKey.isDown());
Profiler.section("Pfadsuche", this::findPath); // HUD-Element „Addon-Profiler“
| Klasse | Wofür |
|---|---|
Events | onAttack (true = abbrechen), onBlockBreak, onDeath, onDimensionChange, onJoin, onLeave – wie die Module-Methoden, als Lambda; jedes liefert ein Tasks.Handle zum Abmelden |
Chat.onReceive | Eingehende Nachrichten: null = ausblenden, andere Nachricht = statt dessen zeigen. Braucht chat |
Inventory | find(item) (Slot oder -1), count(item), selected(), swap(slot) / swap(item) – in die Hand holen. Hotbar = Slot wählen; sonst ein Inventar-Klick, nur wenn der Server "inventory" erlaubt und kein Behälter offen ist |
Waypoints | Gemeinsame Wegpunkte aller Addons je Server/Welt: add, remove (nur eigene), all, dimension |
Camera | offset(vec3), zoom(faktor) (dritte Person), freelook(an) (Server-Funktion "freelook") – je Addon, wie View.fov |
Screenshots | capture(img -> …): Bild im Speicher statt Datei; png(img) = PNG-Bytes (z. B. zum Hochladen mit Web.post). Das Bild gehört dir: close() |
Storage.shared(key) | Werte, die mehrere Addons teilen (gleiche Methoden wie AddonConfig). Jedes Addon kann sie lesen und schreiben – nichts Geheimes ablegen |
Profiler.section(name, run) | Eigene Abschnitte messen; Events und HUD-Elemente misst der Client selbst. Anzeige: HUD-Element „Addon-Profiler“ (Mittel je Bild, rot ab 2 ms) |
Anim.tween(from, to, ms, easing) | Weiche Übergänge für UI-Werte: value() jedes Bild, to(neu) ohne Sprung; Kurven LINEAR, IN, OUT, IN_OUT, BACK |
Hud.compass, Hud.arrow | Kompassleiste mit Hud.Markern; Pfeil am Bildschirmrand zu einem Ziel außerhalb des Bilds (in einem Element über den ganzen Bildschirm) |
Alles ist nur bei dir zu sehen, Server und Hitboxen bleiben gleich. Was durch Wände sichtbar wäre (seeThrough, Highlight), zeigt der Client nur, solange der Server die Funktion "esp" nicht abschaltet. Wird dein Addon entladen oder vom Server abgeschaltet, verschwindet alles davon. Im Render-Thread aufrufen.
Eigener Chat-Befehl@Override
public boolean send(String message) {
if (!message.equals(".ping")) return false;
Minecraft.getInstance().gui.hud.getChat().addClientSystemMessage(Component.literal("Pong!"));
return true; // geht nicht an den Server
}
chatundsendbekommen alle Addons, auch wenn eins schontruegeliefert hat.- Hinter einem Proxy (Bungee, Velocity) kommt beim Serverwechsel erneut
join, ohneleavedazwischen. playerJoinedschweigt in den ersten 2 Sekunden nach dem Betreten – sonst käme jeder, der schon da ist. Alle jetzt:ServerInfo.players().- Wirft dein Addon in einem Event, bekommt es genau dieses Event nicht mehr (Eintrag im Log) – der Rest läuft weiter.
Tasten
Eigene Tasten gibst du in keys() zurück. Sie erscheinen unter Einstellungen → Tastenbelegung in einer Kategorie deines Addons, Spieler belegen sie dort um – gespeichert wird automatisch.
private final KeyMapping zoom = Keys.key("meinaddon", "zoom", InputConstants.KEY_C);
@Override public List<KeyMapping> keys() { return List.of(zoom); }
@Override public void tick() {
while (zoom.consumeClick()) Notify.show("Zoom", "an"); // einmal je Druck
boolean held = zoom.isDown(); // oder: gedrückt gehalten
}
Die Standardtaste kommt aus com.mojang.blaze3d.platform.InputConstants (KEY_…) – Minecraft 26.3 nutzt SDL, nicht GLFW. Beschriftungen kommen aus deinen Übersetzungen: key.<addon>.<name> für die Taste, key.category.<addon>.keys für die Kategorie.
Übersetzungen & Ressourcen
Alles unter assets/<addon>/ in deinem Jar lädt der Client als Resource Pack mit – Übersetzungen, Texturen, Sounds. Ein pack.mcmeta brauchst du nicht; nach dem Installieren lädt der Client die Ressourcen sofort neu.
assets/meinaddon/lang/de_de.json{
"key.meinaddon.zoom": "Zoom",
"key.category.meinaddon.keys": "Mein Addon",
"meinaddon.hallo": "Hallo %s!"
}
Component.translatable("meinaddon.hallo", name) // Text in der Sprache des Spielers, en_us als Rückfall
In der Gradle-Vorlage liegen die Dateien unter src/main/resources/assets/, im Repo unter Client/Addons/<id>/resources/assets/.
Benachrichtigungen & Zeichnen
Notify zeigt eine Meldung im HUD (Element „Benachrichtigungen“, rechts über der Hotbar – oben rechts liegen Vanillas Toasts –, im HUD-Editor verschiebbar). Aus jedem Thread aufrufbar; sichtbar nur im Spiel, nicht in Menüs.
Notify.show("Wegpunkt", "Zuhause gesetzt"); // 4 Sekunden
Notify.show(Component.translatable("meinaddon.titel"), null, 8000); // nur Titel, 8 Sekunden
Notify.show("Rüstung", "Helm fast kaputt", helm); // mit Gegenstand als Symbol
Chat.print("Wegpunkt gesetzt"); // nur in den eigenen Chat (Antworten auf Befehle)
Sounds.play("minecraft:block.note_block.pling", 0.6f, 2); // Geräusch nur für dich
Draw zeichnet im Stil des Clients – damit dein HUD aussieht wie der Rest:
| Methode | Bedeutung |
|---|---|
Draw.panel(g, x, y, w, h) | Abgerundeter Hintergrund mit der eingestellten HUD-Deckkraft (bei 0 % nichts) |
Draw.box(g, x, y, w, h, argb) | Abgerundete Fläche in eigener Farbe |
Draw.text(g, text, x, y) | Text in HUD-Farbe mit Schatten, liefert die Breite |
Draw.ui(text), Draw.sub(text) | Component in der Menü-Schrift (Poppins) bzw. als Zwischenüberschrift |
Draw.texture(addon, name, image) | Eigene Textur aus einem NativeImage (z. B. Minimap) → Identifier; Pixel geändert? Draw.upload(id) |
Draw.image(g, id, x, y, w, h, texW, texH) | Textur auf w × h gezeichnet |
Draw.png(addon, name, stream) | Textur aus einem PNG, z. B. getClass().getResourceAsStream("/icon.png") |
Draw.image(g, id, x, y, w, h, u, v, uw, vh, texW, texH, argb) | Ausschnitt einer Textur, getönt – für Sprite-Sheets |
Draw.item(g, stack, x, y) | Gegenstand wie im Inventar (16 × 16, Anzahl, Haltbarkeit) |
Draw.head(g, name, x, y, size) | Kopf eines Spielers aus der Tab-Liste; false = nicht auf dem Server |
Draw.progress(g, x, y, w, h, fraction, argb) | Fortschrittsbalken (0…1) im Client-Stil |
Draw.roundRect(g, x, y, w, h, radius, argb) | Rechteck mit frei wählbarem Eckenradius |
Draw.gradient(g, x, y, w, h, top, bottom) | Farbverlauf von oben nach unten |
Draw.shadow(g, x, y, w, h, size, argb) | Weicher Schatten um ein Rechteck (vorher zeichnen) |
Draw.circle(g, cx, cy, radius, argb) | Gefüllter Kreis |
Draw.tooltip(g, lines, x, y) | Tooltip im Vanilla-Stil, sofort gezeichnet – auch im HUD |
Draw.entity(g, entity, x, y, size, mouseLook, mouseX, mouseY) | Wesen wie im Inventar, optional zur Maus schauend |
Draw.font(addon, name, text) | Text in eigener Schrift aus assets/<addon>/font/<name>.json (TTF-Provider; Größe dort) |
Draw.minimap(g, x, y, size, zoom) | Fertige Karte von oben, Spieler in der Mitte; zoom = Pixel je Block (1–8) |
Draw.scaled(g, scale, x, y, () -> …) | Vergrößert zeichnen, (x, y) bleibt fest |
Draw.clip(g, x, y, w, h, () -> …) | Nur innerhalb des Rechtecks zeichnen (scrollende Listen) |
Draw.project(vec3) | Punkt in der Welt → Position im HUD (Vec2, GUI-Pixel); null = hinter der Kamera. Für Wegpunkte und Markierungen – am besten in einem Element über den ganzen Bildschirm, denn die Position ist absolut. |
HUD-Elemente
Ein HudElement beschreibt Standard-Position und -Größe; Spieler verschieben, skalieren und verstecken es im HUD-Editor. Du zeichnest immer an der Standard-Position – Verschieben und Skalieren legt der Client darüber.
record HudElement(String id, String name, float ax, float ay, int ox, int oy, int w, int h, Renderer render)
Für die häufigste Sorte – eine Textzeile im Client-Stil – reicht eine Zeile: HudElement.text("meinaddon.ping", "Ping", 0, 0, 2, 40, 80, () -> "Ping: " + ping). null als Text blendet sie aus.
HudElement.fit(…) nimmt dieselben Angaben, aber der Hintergrund ist nur so breit wie der Text (rechts verankerte wachsen nach links). element.visibleIf(() -> bedingung) zeichnet ein Element nur, solange die Bedingung gilt.
| Feld | Bedeutung |
|---|---|
id | Eindeutig über alle Addons. Ab 6.1.2 gehören <addon-id> und <addon-id>.… immer diesem Addon; ein Element mit einer fremden oder schon vergebenen ID wird übersprungen (Log-Eintrag). |
name | Anzeigename im HUD-Editor. |
ax, ay | Anker als Bruchteil des Bildschirms: 0 = links/oben, 0.5 = Mitte, 1 = rechts/unten. |
ox, oy | Versatz der linken oberen Ecke vom Anker, in GUI-Pixeln. |
w, h | Größe des Kastens in GUI-Pixeln. |
render | (g, x, y) -> … zeichnet mit Minecrafts GuiGraphicsExtractor; x, y = linke obere Ecke. |
Die linke obere Ecke liegt bei ax × Bildschirmbreite + ox und ay × Bildschirmhöhe + oy. Ändert sich die Größe (z. B. nach einer Einstellung), liefere in hud() einfach ein neues Element – der Client fragt nach dem Schließen der Einstellungen erneut.
HUD-Farbe
Spieler stellen eine gemeinsame HUD-Farbe ein (fest oder als Farbverlauf). Nutze sie, damit dein Addon zum Rest passt:
int text = HudColor.at(x, y); // ARGB-Textfarbe an dieser Stelle (Farbverlauf beachtet)
int box = HudColor.background(); // Hintergrund mit eingestellter Deckkraft
Einstellungen
Gib in options() eine Liste aus Setting-Bausteinen zurück. Der Client zeichnet sie unter Einstellungen → Addons im Reiter deines Addons – im selben Stil wie seine eigenen Einstellungen, durchsuchbar über die Suche. Werte hältst du selbst: get liest, set schreibt und speichert.
| Baustein | Darstellung |
|---|---|
Setting.heading(text) | Zwischenüberschrift |
Setting.info(() -> text) | Infozeile, wird laufend aktualisiert (Statistiken, Hinweise) |
Setting.toggle(name, beschreibung, get, set) | Schalter an/aus |
Setting.slider(name, beschreibung, min, max, get, set, format) | Schieberegler über ganze Zahlen; format darf null sein |
Setting.choice(name, beschreibung, werte, get, set, format) | Auswahl ‹ Wert › aus einer festen Liste |
Setting.button(name, beschreibung, beschriftung, aktion) | Knopf rechts in der Zeile; danach wird die Seite neu aufgebaut |
Setting.text(name, beschreibung, maxLänge, get, set) | Texteingabe |
Setting.color(name, beschreibung, get, set) | Farbe (0xRRGGBB), klappt den Farbwähler der HUD-Farbe auf |
Setting.key(taste) | Taste aus keys() direkt hier belegen |
Setting.decimal(name, beschreibung, min, max, schritt, get, set, format) | Schieberegler über Kommazahlen |
Setting.multi(name, beschreibung, werte, get, set, format) | Mehrfachauswahl (Set), klappt je Wert einen Schalter auf |
Setting.list(name, beschreibung, maxLänge, get, set) | Textliste mit ▴ ▾ zum Umsortieren, Papierkorb und „Hinzufügen“ – passt zu cfg.list |
….visibleIf(() -> bedingung) | An jeden Baustein anhängbar: nur zeigen, solange die Bedingung gilt – z. B. Unteroptionen eines Schalters. Nach jedem Schalter und jeder Auswahl neu geprüft. |
Setting.custom(panel) | Eigenes Bedienelement über die volle Breite, siehe Eigene Elemente |
Die Beschreibung erscheint grau unter dem Namen und vollständig als Tooltip; null lässt sie weg.
@Override
public List<Setting> options() {
return List.of(
Setting.heading("Anzeige"),
Setting.toggle("Schatten", "Text mit Schatten zeichnen",
() -> cfg.bool("shadow", true), v -> cfg.set("shadow", v)),
Setting.slider("Größe", null, 50, 200,
() -> cfg.number("size", 100), v -> cfg.set("size", v), v -> v + " %"),
Setting.choice("Position", null, List.of("links", "rechts"),
() -> cfg.text("pos", "links"), v -> cfg.set("pos", v), v -> v),
Setting.info(() -> "Gezeichnet: " + counter + " mal"),
Setting.button("Zurücksetzen", "Alle Werte auf Standard", "Zurücksetzen", cfg::clear));
}
options() läuft bei jedem Öffnen und Umschalten. Erzeuge die Liste ruhig jedes Mal neu – so stimmen Texte nach einem Sprachwechsel und Werte, die sich woanders geändert haben.Werte speichern
AddonConfig speichert Schlüssel/Wert-Paare in .minecraft/client-modules/<id>.properties. Gelesen wird beim ersten of(id), geschrieben bei jedem set.
var cfg = AddonConfig.of("meinaddon");
boolean an = cfg.bool("enabled", true); // Standardwert, falls nicht gespeichert
int größe = cfg.number("size", 100);
String pos = cfg.text("pos", "links");
double zoom = cfg.decimal("zoom", 1.5);
List<String> namen = cfg.list("friends", List.of());
cfg.set("friends", List.of("Steve", "Alex")); // Listen: Einträge ohne Zeilenumbruch
Path ordner = AddonConfig.dir("meinaddon"); // eigener Ordner für Dateien: client-modules/meinaddon/
var hier = AddonConfig.forServer("meinaddon"); // nur für diesen Server bzw. diese Welt (z. B. Wegpunkte)
cfg.set("size", 120); // speichert sofort
cfg.clear(); // alles auf Standard
render), liest du einmal in ein Feld und aktualisierst es im set – wie seconds im Schnellstart.Eigene Elemente
Reicht keine Zeile – etwa für einen Layout-Editor wie bei Keystrokes – liefert Setting.custom ein eigenes Minecraft-Widget über die volle Breite:
Setting.custom((width, relayout) -> editor.create(width, relayout))
- Das Widget ist ein normales
net.minecraft.client.gui.components.AbstractWidget: zeichnen inextractWidgetRenderState, Maus überonClick,onDrag,onRelease, Tasten überkeyPressed. - Ändert sich die Höhe, ruf
relayout.run()auf. Die Seite wird neu aufgebaut und das Widget neu angefordert – halte den Zustand (Auswahl usw.) darum im Addon, nicht nur im Widget. Am einfachsten: immer dieselbe Widget-Instanz zurückgeben. - Wartet dein Widget auf eine Taste, implementiere
Setting.Captureund gib incapturing()truezurück. Dann bekommt es alle Tasten zuerst – auch Esc, das sonst die Einstellungen schließt.
Eigene Seiten
Braucht dein Addon mehr Platz als den Reiter unter Einstellungen → Addons – etwa eine Liste von Wegpunkten –, öffnet Page.open einen eigenen Bildschirm im Stil des Clients: oben „Zurück“ und dein Titel, darunter Zeilen aus denselben Setting-Bausteinen.
Page.open("Wegpunkte", () -> waypoints.stream()
.map(w -> Setting.button(w.name(), w.pos().toShortString(), "Löschen", () -> waypoints.remove(w)))
.toList());
Die Liste wird bei jedem Aufbau neu abgefragt (nach jedem Knopf), darf sich also ändern. Für ganz eigene Oberflächen: Setting.custom mit einem eigenen Widget. „Zurück“ führt zum vorher offenen Bildschirm.
Brauchst du die ganze Fläche – Vollbild-Karte, Statistiken, ein Mini-Spiel –, gib statt der Liste eine Page.View. Gezeichnet wird unter „Zurück“ und Titel; Koordinaten beginnen oben links an der Fläche, in Einheiten der Client-Menüs.
Page.open("Karte", new Page.View() {
public void draw(GuiGraphicsExtractor g, int w, int h, int mouseX, int mouseY) {
Draw.image(g, map, 0, 0, w, h, 128, 128);
}
public boolean drag(double x, double y, int button, double dx, double dy) { pan(dx, dy); return true; }
public boolean scroll(double x, double y, double amount) { zoom(amount); return true; }
});
Außerdem gibt es click, key und closed. Esc schließt immer. Wirft die View, bleibt die Fläche leer und du bekommst einen Absturzbericht.
Fertige Bedienelemente statt selbst zeichnen: Widgets baut Knöpfe, Schalter, Regler, Textfelder, Auswahl und Listen und ordnet sie mit row, column und grid an – ohne Koordinaten.
var name = Widgets.textField("Name", "", s -> {});
Page.open("Wegpunkt", Widgets.view(Widgets.column(
name,
Widgets.row(Widgets.toggle("Sichtbar", true, v -> {}), Widgets.slider("Größe", 1, 10, 3, v -> {})),
Widgets.list(waypoints, Waypoint::name, w -> select(w)), // füllt den Rest
Widgets.button("Speichern", () -> save(name.text())))));
// In fremde Menüs: Knopf oben rechts ins Inventar
Screens.inject(InventoryScreen.class, Widgets.view(Widgets.button("Sortieren", this::sort), -8, 8, 80, 20));
Screens.inject und onOpen nehmen Vanilla-Klassen, auch wenn der Client das Menü ersetzt (Titel, Pause, Optionen). Overlays bekommen Klicks zuerst; was sie nicht verbrauchen, geht ans Menü. Notify.action zeigt Meldungen mit Knöpfen – klickbar, sobald die Maus frei ist (im Spiel: Chat öffnen).
Addons untereinander
Jedes Addon hat seinen eigenen ClassLoader – Klassen eines anderen Addons kannst du nicht direkt benutzen. Stattdessen schickt der Bus Nachrichten über ein Thema, mit Werten aus JDK- oder Minecraft-Typen (String, Map, BlockPos …):
// Minimap-Addon: empfängt Wegpunkte
Bus.on("minimap/waypoint", msg -> waypoints.add((BlockPos) msg));
// anderes Addon: schickt einen – liefert, wie viele Empfänger es gab (0 = Minimap nicht installiert)
int n = Bus.send("minimap/waypoint", player.blockPosition());
Themen beginnen am besten mit der Addon-ID des Empfängers. Zugestellt wird sofort im Thread des Senders.
Bus.answer("minimap/waypoints", q -> List.copyOf(waypoints)); // Minimap beantwortet Fragen
var list = (List<?>) Bus.ask("minimap/waypoints", null); // null = niemand antwortet
Bus.off("minimap/waypoint", listener); // Empfänger wieder abmelden
Befehle
Eigene Chat-Befehle mit / – das Chatfeld schlägt sie vor wie Vanilla-Befehle (Tab), ausgeführt werden sie nur im Client, der Server sieht sie nie:
Commands.register("wp", args -> { // /wp add Zuhause
if (args.length == 2 && args[0].equals("add")) waypoints.add(args[1]);
}, args -> args.length == 1 ? List.of("add", "remove", "list") : List.of());
Jeder Befehl ist zusätzlich als /<addon-id>:<name> erreichbar (z. B. /minimap:wp). Hat ein anderes Addon den kurzen Namen schon, bleibt er dort – dein Befehl ist dann nur mit Präfix da, und der Spieler bekommt einen Hinweis im Chat.
Der zweite Teil liefert Vorschläge für das gerade getippte Wort (null = keine). Gleichnamige Server-Befehle werden überdeckt – nimm Namen, die eindeutig zu deinem Addon gehören. Ist dein Addon auf einem Server abgeschaltet, gehen seine Befehle dort normal an den Server.
Aufgaben
Tasks.later(40, () -> Notify.show("Hallo", "zwei Sekunden später")); // Ticks: 20 = 1 s
var h = Tasks.every(20, this::refresh); // jede Sekunde, bis h.cancel()
Tasks.main(() -> …); // aus eigenem Thread zurück in den Render-Thread
Beim Entladen räumt der Client die Aufgaben ab; auf Servern, die dein Addon abschalten, pausieren sie.
Internet
Web holt Daten aus dem Internet – nur mit der Berechtigung "network", sonst schlägt das Ergebnis mit einer SecurityException fehl. Die Antwort kommt in einem fremden Thread, darum zurück mit Tasks.main:
Web.get("https://api.example.com/stats?name=Steve")
.thenAccept(json -> Tasks.main(() -> stats = parse(json)))
.exceptionally(e -> { Notify.show("Stats", "nicht erreichbar"); return null; });
Web.post(url, body, "application/json"); // POST, ebenso als CompletableFuture<String>
Nur http/https, Zeitlimit 15 Sekunden; Status außerhalb von 2xx ist ein Fehler.
Freunde & Chat
Der Client hat eine eingebaute Freundesliste mit privatem Chat. Spieler öffnen sie im Hauptmenü (Freunde), im Pausemenü oder mit /fmsg, fügen Freunde über den Spielernamen hinzu und nehmen Anfragen an. Mit Friends liest dein Addon, wer befreundet und online ist, bekommt neue Nachrichten und kann im Namen des Spielers schreiben. Hinzufügen und Entfernen geht nur über die Oberfläche, nicht über die API. Braucht die Berechtigung "friends", sonst werfen list, get, onMessage und onChange eine SecurityException und send schlägt fehl.
module.json"permissions": ["friends"]
Freunde-API (ab 6.2.1)@Override
public void ready() {
Friends.onMessage(m -> { // jede neue Nachricht, auch eigene (m.mine())
if (!m.mine()) Sounds.play("minecraft:block.note_block.bell");
});
Friends.onChange(() -> online = Friends.list().stream().filter(Friends.Friend::online).count());
}
void invite(String name) {
var f = Friends.get(name); // null = kein Freund
if (f == null || !f.online()) return;
Friends.send(f.uuid(), "Komm auf " + ServerInfo.address() + "!")
.thenAccept(m -> Tasks.main(() -> Notify.show("Freunde", "Einladung an " + f.name() + " gesendet")))
.exceptionally(e -> { Tasks.main(() -> Notify.show("Freunde", e.getMessage())); return null; });
}
| Methode | Bedeutet |
|---|---|
Friends.available() | Mit dem Freunde-Dienst verbunden. false bei Offline-Accounts (kein Microsoft-Login) oder wenn der Client-Server nicht erreichbar ist – dann ist die Liste leer. Ohne Berechtigung. |
Friends.list() | Alle Freunde als Friend(uuid, name, online), online zuerst, sonst nach Name; unveränderlich |
Friends.get(uuid) · Friends.get(name) | Ein Freund oder null – auch als „ist befreundet?“; Name ohne Rücksicht auf Groß-/Kleinschreibung |
Friends.send(uuid, text) | Nachricht im Namen des Spielers, CompletableFuture<Message>; erscheint bei beiden im Freunde-Chat. Höchstens Friends.MAX_LENGTH (256) Zeichen, nur an Freunde |
Friends.onMessage(m -> …) | Jede neue Nachricht, empfangen und selbst geschrieben, sobald sie da ist – auch im Menü. Den Verlauf bekommst du hier nicht |
Friends.onChange(() -> …) | Liste oder Online-Status hat sich geändert, auch beim Verbinden und Trennen |
Friends.open() · Friends.open(uuid) | Freunde-Seite öffnen, optional gleich mit der Unterhaltung; ohne Berechtigung |
Message(id, from, to, text, time): id steigt mit jeder Nachricht, time = Millisekunden seit 1970 (Serverzeit), mine() = selbst geschrieben (auch von einem anderen eigenen Client), partner() = der andere in der Unterhaltung. Texte sind reiner Text ohne §-Farbcodes und Zeilenumbrüche – der Server entfernt sie.
Listener laufen im Render-Thread; das Ergebnis von send kommt in einem fremden Thread, darum zurück mit Tasks.main. Wirft ein Listener, wird er abgemeldet (Absturzbericht). Beim Entladen des Addons verschwinden alle Listener, auf Servern, die dein Addon abschalten, ruhen sie. Nachrichten kommen sofort an, der Online-Status nach spätestens etwa 25 Sekunden; online heißt „in der letzten Minute mit dem Client verbunden“, nicht „auf demselben Server“.
Verschick nichts ungefragt: send schreibt als der Spieler, und Freunde sehen es wie selbst getippt. Spieler sehen die Berechtigung vor dem Installieren und bei der Freigabe.
Für Spieler
/fmsgöffnet die Freunde-Seite,/fmsg <name>die Unterhaltung,/fmsg <name> <text>schickt direkt aus dem Spiel (Tab ergänzt Namen). Der Server sieht den Befehl nie.- Neue Nachrichten erscheinen im Spiel als Chatzeile mit ✉ – anklicken, um zu antworten. Anfragen kommen als Meldung mit Annehmen/Ablehnen, außerdem eine Meldung, wenn ein Freund online kommt.
- Finden kannst du nur Spieler, die sich schon einmal mit diesem Client angemeldet haben (Microsoft-Account). Der Verlauf hält die letzten 100 Nachrichten je Freund.
Sichtfeld & Maus
View ändert Sichtfeld und Mausempfindlichkeit ohne eigene Patches – für Zoom-Addons reicht das:
public void tick() {
boolean z = zoom.isDown() && ServerInfo.allowed("zoom");
View.fov(z ? 0.25f : 1); // 4× Zoom; 1 = unverändert
View.sensitivity(z ? 0.25f : 1); // Maus entsprechend langsamer
}
Faktoren gelten je Addon und multiplizieren sich. Wird dein Addon entladen oder auf einem Server abgeschaltet, fällt sein Faktor weg.
Server-Kommunikation
Mit Net tauscht dein Addon Daten mit einem Server-Plugin aus – über Plugin-Channels, wie Bukkit/Paper (Player.sendPluginMessage), BungeeCord und Velocity sie kennen. Braucht die Berechtigung "server".
Net.on("minimap:waypoints", data -> load(new String(data, StandardCharsets.UTF_8)));
Net.send("minimap:hello", "1.0".getBytes(StandardCharsets.UTF_8)); // false = mit keinem Server verbunden
Paper-Plugin (Gegenstück)getServer().getMessenger().registerOutgoingPluginChannel(this, "minimap:waypoints");
player.sendPluginMessage(this, "minimap:waypoints", json.getBytes(StandardCharsets.UTF_8));
Channels heißen namespace:pfad, am besten <addon-id>:<thema>; client: und minecraft: sind reserviert. Der Client meldet alle Channels beim Betreten an (minecraft:register) – erst danach schicken Bukkit-Server etwas. Empfänger laufen im Render-Thread, höchstens 1 MB je Paket.
Server-Infos & Eingabe
Was Ping-, TPS-, CPS- und Spieler-HUDs brauchen, sammelt der Client für alle Addons:
ServerInfo.ping() // eigener Ping in ms (Tab-Liste), -1 = unbekannt
ServerInfo.tps() // Ticks pro Sekunde des Servers, geschätzt (höchstens 20)
ServerInfo.brand() // "Paper", "vanilla" …; null = unbekannt
ServerInfo.address() // Adresse aus der Serverliste, null = Einzelspieler
ServerInfo.players() // Namen in der Tab-Liste
Input.cps(0) // Linksklicks der letzten Sekunde (1 = rechts, 2 = Mitte, 3+ Seitentasten)
ServerInfo.scoreboardTitle(), ServerInfo.scoreboard() // Scoreboard rechts: Titel + Zeilen (Component)
ServerInfo.tabHeader(), ServerInfo.tabFooter() // Kopf-/Fußzeile der Tab-Liste; null = keine
ServerInfo.bossbars() // Bossbars: name, progress (0…1)
ServerInfo.allowed("freelook") // false = Server verbietet diese Funktion (Server-Plugin)
Input.cps zählt jeden Klick im Spiel, auch zwei zwischen zwei Bildern – genauer als selbst je Bild abzufragen. Keine Berechtigung nötig: es sind nur Zahlen, keine Tasten.
Andere Addons & Versionen
if (Addons.installed("minimap")) Bus.send("minimap/waypoint", pos); // optionale Abhängigkeit, ohne "requires"
String v = Addons.version("minimap"); // "1.2.0"; null = nicht geladen
if (Addons.atLeast("6.1.0")) View.fov(0.5f); // API des laufenden Clients (Semver)
Addons.active("minimap") // geladen und auf diesem Server nicht abgeschaltet
Module.VERSION baut der Compiler in dein Addon ein – das ist die Version, gegen die du gebaut hast, nicht die des Spielers. Zur Laufzeit zählt Addons.apiVersion() bzw. Addons.atLeast(…); Addons.compare(a, b) vergleicht zwei Versionen. Addons.api() liefert nur die Hauptversion (6).
@since in der API-Referenz und in deiner IDE.Server-Kontrolle
Server bestimmen, welche Addons bei ihnen laufen. Dafür hat jedes veröffentlichte Addon eine UID: 8 Zeichen, vom Addon-Server bei der ersten Freigabe vergeben, ändert sich nie (auch nicht bei Updates). Sie steht im Katalog auf der Website und im Spiel auf der Addon-Seite.
Server-Betreiber legen das Plugin server-plugin/build/libs/client-addons.jar (Paper 26.3, gradlew build in server-plugin/) in ihren plugins/-Ordner und tragen in plugins/ClientAddons/config.yml ein, was aus sein soll:
plugins/ClientAddons/config.ymldisabled: # einzelne Addons: UID (empfohlen) oder ID
- a7k2m9x4
disabled-categories: # ganze Kategorien
- pvp
allow-unverified: true # selbst installierte Addons ohne UID erlauben?
disabled-features: # einzelne Funktionen, die Addons per ServerInfo.allowed prüfen (ab API 6.1.0)
- freelook
- Beim Betreten meldet der Client seine Addons (
client:addons), das Plugin antwortet mit den Vorgaben (client:policy). Abgeschaltete Addons bekommen dort keine Events, Befehle und Aufgaben, ihre HUD-Elemente sind aus; der Spieler sieht eine Benachrichtigung. Beim Verlassen ist alles wieder an. /clientaddons <spieler>zeigt die Addons eines Spielers samt UID,/clientaddons reloadlädt die Konfiguration neu und schickt sie an alle (Berechtigungclientaddons.admin).- Diese Funktionsnamen prüft der Client selbst (für alle Addons):
esp– alles, was durch Wände sichtbar wäre (Highlight,seeThroughbeiRender);freelook–Camera.freelook;inventory–Inventory.swapaußerhalb der Hotbar (Inventar-Klick). Weitere Namen legen Addons selbst fest und prüfen sie mitServerInfo.allowed. - Das ist eine Absprache, kein Anti-Cheat: Addons sind normales Java, und wer den Client verändert, kann sie umgehen. Für ehrliche Spieler und Addons heißt es aber: auf jedem Server automatisch regelkonform.
Patches
Für Eingriffe, die keine API abdeckt (Chunk-Bau, Netzwerk, eigene Shader-Pipelines … – Welt-Rendering, Partikel und Kamera gehen ab 6.2.0 ohne Patches), baut der Loader Aufrufe deiner Hook-Methoden direkt in Minecraft-Methoden ein. So arbeitet das Performance-Addon.
module.json{
"name": "Mein Addon",
…
"hooks": "meinaddon/Hooks",
"patches": [
{
"cls": "net/minecraft/client/particle/ParticleEngine",
"method": "add",
"desc": "(Lnet/minecraft/client/particle/Particle;)V",
"hook": "particle",
"replace": true
}
]
}
| Feld | Bedeutung |
|---|---|
cls | Zielklasse als interner Name: / statt ., innere Klassen mit $. |
method | Methodenname, <init> für Konstruktoren. |
desc | Exakter JVM-Deskriptor. Anzeigen mit javap -s -p -cp client/.cache/26.3.jar <Klasse>. Darf fehlen, wenn die Klasse nur eine Methode dieses Namens hat – sonst meldet der Build „mehrdeutig“. |
hook | Name der public static-Methode in deiner hooks-Klasse. |
replace | true = Ersetzen am Methodenanfang, sonst Anhängen am Ende. |
Ersetzen ("replace": true)
Läuft am Anfang der Methode: boolean hook([this,] parameter…). Gibt der Hook true zurück, wird Minecrafts Code übersprungen; Methoden mit Rückgabewert liefern dann null, 0 bzw. false.
// ParticleEngine.add(Particle) – über dem Limit keine neuen Partikel
public static boolean particle(ParticleEngine engine, Particle p) {
return count(engine) >= LIMIT;
}
Anhängen (Standard)
Läuft vor jedem return: R hook([rückgabewert,] [this,] parameter…) – der Rückgabewert des Hooks ersetzt den von Minecraft. Bei void-Methoden entfällt beides.
// EntityRenderDispatcher.shouldRender(...) – Vanilla sagt sichtbar, wir prüfen zusätzlich
public static boolean entity(boolean vanilla, EntityRenderDispatcher d, Entity e, Frustum f,
double cx, double cy, double cz, float partialTick) {
return vanilla && !hiddenBehindWall(e);
}
Regeln für Hook-Signaturen
this(bei Instanzmethoden) und Rückgabewert haben den exakten Typ.- Parameter haben ihren exakten Typ – außer innere Klassen (Deskriptor enthält
$): die kommen alsObject, weil sie oft nicht öffentlich sind. - Mehrere Patches auf dieselbe Methode sind erlaubt, z. B. Ersetzen am Anfang und Anhängen am Ende.
- Der Build prüft jeden Patch gegen das echte Minecraft-Jar: Ziel vorhanden, Hook-Signatur passend, Bytecode gültig. Ein Fehler bricht den Build ab – statt das Spiel beim Spieler abstürzen zu lassen. In der Gradle-Vorlage macht das
gradlew check(läuft auch beibuild).
Tests
Jede .java-Datei in Client/Addons/<id>/test/ wird nach dem Kompilieren mit java ausgeführt, dein Addon und Minecraft liegen im Classpath. Wirft sie eine Ausnahme oder endet mit Fehlercode, schlägt der Build fehl. Kein Framework nötig:
Client/Addons/meinaddon/test/MathCheck.javaimport meinaddon.Mathe;
public class MathCheck {
public static void main(String[] args) {
if (Mathe.clamp(15, 0, 10) != 10) throw new AssertionError("clamp");
System.out.println("MathCheck OK");
}
}
Test-Dateien liegen im Standardpaket – testbare Klassen deines Addons müssen darum public sein.
Testen & Hot-Reload
gradlew runClient startet das Spiel mit dem Client und deinem Addon – ohne Launcher und Login: offline als „Dev“ (anderer Name: -Pplayer=…), Spielordner run/ im Projekt. Mit -Pserver=localhost:25565 geht es gleich auf einen Server; Offline-Spieler kommen nur auf Server mit online-mode=false, etwa einen lokalen Vanilla-Server.
Starten & Debuggen aus der IDE
Die Vorlage bringt fertige Konfigurationen mit – nach dem Öffnen des Ordners sind sie sofort da:
| IDE | Konfigurationen | Debuggen |
|---|---|---|
IntelliJ IDEA (.run/) | „Addon bauen“, „Client starten · MC 26.3“, „Addon testen · MC 26.3“, „An Client anhängen (Port 5005)“ | „Client starten“ mit dem Debug-Knopf starten – IntelliJ hängt sich selbst an. Breakpoints und Hot-Swap (Methodenrümpfe) gehen direkt. |
VS Code (.vscode/, „Extension Pack for Java“) | Tasks „Client starten · MC 26.3“, „Addon testen · MC 26.3“ | Start-Konfiguration „Client debuggen · MC 26.3“ (F5): startet mit --debug-jvm und hängt sich an. |
| Andere / Terminal | gradlew runClient | gradlew runClient --debug-jvm, dann Remote-Debugger auf localhost:5005. |
Minecraft-Version: Ohne Angabe gilt minecraft aus der module.json. Mit -Pminecraft=… baust, startest und testest du gegen eine andere – aber nur eine, auf der der Client läuft (sonst bricht runClient mit der Liste ab). Welche das sind, steht im Client-Jar (gradlew minecraftVersions), aktuell 26.3. Kommt eine neue dazu, kopier eine Konfiguration und ändere die Zahl.
Geändert? gradlew install legt das neue Jar neben das laufende (<id>.jar.update), im Chat lädt es dann ohne Neustart:
Automatisch testen: gradlew runTest lädt einen Vanilla-Server, startet ihn (Port 25598, offline) und das Spiel mit dem Flag client.test – dein Addon prüft im Spiel und meldet das Ergebnis. Der Build schlägt fehl bei einer fehlgeschlagenen Prüfung oder nach 6 Minuten ohne Ergebnis.
public void join(String server) {
if (Testing.active()) Tasks.later(40, () -> {
Testing.check(meinHud != null, "HUD gebaut");
Testing.done(); // Ergebnis schreiben, Spiel beenden
});
}
Testing.command("give @s diamond 3"); // Testspieler ist Operator
Testing.await("Diamanten da", () -> anzahl() == 3, 100, Testing::done); // jeden Tick prüfen, höchstens 5 s
Die Vorlage bringt .github/workflows/build.yml mit: baut bei jedem Push (Repository-Variable CLIENT_SERVER = Adresse des Addon-Servers) und legt das Jar als Artefakt ab. runTest braucht Grafik und läuft darum lokal.
Geändert, während das Spiel läuft:
.addon list # geladene Addons
.addon reload meinaddon # entladen (unload) und neu laden – mit dem neuen Jar
.addon reload # alle
- Addons mit Patches brauchen weiter einen Neustart.
- Bringt dein Addon Ressourcen mit (
assets/), hält das Resource Pack das alte Jar offen – dann lädt der Reload den alten Stand und das Update kommt beim nächsten Start. Für Code-Änderungen ohne Neustart solange Ressourcen weglassen. - Assets (~600 MB) nimmt
runClientvom Launcher oder vom offiziellen Launcher mit, sonst lädt es sie einmalig.
Veröffentlichen
versionin dermodule.jsonerhöhen.gradlew publishAddon -PaddonToken=<token>– den Token bekommst du vom Betreiber des Addon-Servers. Der Server prüft das Jar (module.json, Service-Datei, API-Version, Ressourcen nur unterassets/<id>/) und stellt es in die Warteschlange.- Nach der Freigabe liefert der Server es im Katalog aus – im Spiel und auf dieser Website. Spieler mit älterer Version sehen ein Update. Eine ID gehört dem, der sie zuerst veröffentlicht hat.
Für Betreiber des Addon-Servers:
node Server/Backend/index.mjs token <entwickler> # Token anlegen
node Server/Backend/index.mjs pending # wartende Uploads
node Server/Backend/index.mjs approve <id> # freigeben
node Server/Backend/index.mjs reject <id> # verwerfen
Addons dieses Repos (Client/Addons/<id>) baut weiter npm run build:client direkt nach Server/Backend/modules/.
Absturzberichte
Wirft ein veröffentlichtes Addon im Spiel (Event, Befehl, Laden), schickt der Client den Fehler einmal je Sitzung an den Addon-Server: Version, Stelle, Fehler und Stacktrace – keine Spielerdaten. Selbst installierte Addons melden nichts. Abrufen mit deinem Token:
curl -H "Authorization: Bearer <token>" https://<addon-server>/modules/<id>/crashes
node Server/Backend/index.mjs crashes <id> # Betreiber, auf dem Server
Server/Backend/data/signing-key.pem, geheim halten); der Client installiert nur Jars mit gültiger Signatur. Den Server im Internet trotzdem nur hinter HTTPS betreiben.Tipps
- Mehrsprachig: Lang-Dateien unter
assets/<addon>/lang/undComponent.translatable, siehe Übersetzungen. - Render-Code sparsam halten: HUD-Elemente laufen jedes Bild. Texte, die sich selten ändern, zwischenspeichern statt jedes Mal neu zusammenzubauen.
- Fehler sind isoliert: Wirft dein Addon beim Laden, beim HUD-Zeichnen oder in
options(), überspringt der Client nur dieses Addon bzw. Element und schreibt den Fehler ins Log. - Vorbilder: Die mitgelieferten Addons
Client/Addons/keystrokes(HUD und eigener Editor) undClient/Addons/performance(Patches) zeigen alle Bausteine im Einsatz.
Changelog
Die API-Version steht in Module.VERSION (Semver, ab 6.0.1); trag sie als "api" in deine module.json ein, z. B. "6.1.0". Ältere Addons laufen auf neueren Clients weiter. Die API-Referenz gibt es für jede Version – baust du gegen eine ältere, stell sie oben um.
| API | Neu |
|---|---|
6.2.1 | Freunde & Chat: Friends.list/get/available, Friends.send (im Namen des Spielers), Friends.onMessage/onChange, Friends.open · neue Berechtigung "friends" (gilt auch für Addons von vor API 5 nur, wenn angegeben) · im Client: Freunde-Seite im Hauptmenü und Pausemenü, Anfragen als Meldung, Befehl /fmsg |
6.2.0 | Welt-API: Render.box/line/text3d/item3d/beam · GhostBlocks (Blöcke und .glb-Modelle nur für dich) · Particles (Vanilla, eigene mit Spec, emitter) · Highlight · BlockTint · Sky.color/fog/clouds · Weather.visual · Light.overlay · Decals · PostFx · Entities.renderIf/scale/tint · Trails · Server-Funktion "esp" für alles, was durch Wände sichtbar wäre · UI: Widgets (button/toggle/slider/textField/dropdown/list, Layout row/column/grid, view) · Draw.roundRect/gradient/shadow/circle/tooltip/entity/font/minimap · Anim.tween · Screens.inject/onOpen (auch für vom Client ersetzte Menüs) · Notify.action mit Knöpfen · Hud.compass/arrow · Page.View.type (Zeicheneingabe) · Events.onAttack/onBlockBreak/onDeath/onDimensionChange/onJoin/onLeave · Chat.onReceive (filtern/umschreiben, Berechtigung chat) · Inventory.find/count/swap (Tauschen außerhalb der Hotbar: Server-Funktion "inventory") · Waypoints (gemeinsam, je Server) · Camera.offset/zoom/freelook (Server-Funktion "freelook") · Screenshots.capture/png · Storage.shared · Profiler.section + HUD-Element „Addon-Profiler“ |
6.1.2 | Eigenes Zeichnen: Page.open mit Page.View (freie Fläche) · Draw.png/item/head/progress/scaled/clip, Draw.image mit Ausschnitt und Tönung · Events tooltip und itemOverlay · Vorlage: Start- und Debug-Konfigurationen für IntelliJ und VS Code, -Pminecraft=… und gradlew minecraftVersions (vom Client unterstützte Versionen) · Kein Addon kann sich mehr als ein anderes (oder als der Client) ausgeben: Keys, AddonConfig und Draw.texture nehmen nur noch die eigene Addon-ID (= Jar-Name), sonst SecurityException · Berechtigungen gelten schon im Konstruktor, in ready() und für Code aus eigenen ClassLoadern (die haben keine) · „Für den Client“-Methoden (Net.receive, Bus/Tasks/Commands/View.drop, Tasks.tick, Input.click, ServerInfo.time) werfen für Addons · Commands.get/all liefern Addons nur eigene Befehle · jeder Befehl auch als /<addon-id>:<name>, Hinweis im Chat bei Namensgleichheit · HUD-IDs <id> und <id>.… gehören dem Addon <id>, fremde und doppelte werden übersprungen · Absturzberichte nur noch aus dem Client (Faults) |
6.1.1 | Korrekturen, keine neuen Bausteine: Berechtigungen am ClassLoader statt am Paketnamen geprüft (ein Addon im Paket client.* umging sie) · Net.send lehnt Pakete über 32767 Bytes ab (sonst trennt der Server) · AddonConfig: eine Instanz je Datei, speichert gesammelt höchstens einmal je Sekunde (und beim Entladen/Beenden) · Notify: höchstens 2 Meldungen je Addon gleichzeitig · Commands: Namen eines anderen Addons werden nicht mehr überschrieben · Bus erreicht keine abgeschalteten Addons · Web: Antworten höchstens 10 MB, Zeichensatz aus dem Content-Type · Fehler in Aufgaben, Bus- und Net-Empfängern erzeugen Absturzberichte · HudElement.fit: Mitte = Anker 0.4–0.6 · weniger Kopien je Tick für equipment/effect |
6.1.0 | View.fov/sensitivity (Zoom ohne Patches) · Notify mit Gegenstand als Symbol · Events equipment, xp, food, effect, screenshot · ServerInfo.scoreboard/scoreboardTitle/tabHeader/tabFooter/bossbars · Sounds.play · HudElement.fit, visibleIf · ServerInfo.allowed + disabled-features im Server-Plugin · Testing.command/await · Setting.list |
6.0.1 | Semver-Versionen (Module.VERSION, Addons.apiVersion/atLeast/compare) · Chat.print · Event blocked · Addons.active · Web.get/post (Berechtigung "network" wird jetzt geprüft) · Bus.off/answer/ask |
6 | ServerInfo (Ping, TPS, Brand, Spieler) · Input.cps · Draw.project · Events tickStart, dimension, health, container, pickup, playerJoined, playerLeft, hudHidden · Setting.decimal/multi, visibleIf · Addons (api(), installed, version) · @since in der Javadoc · Build prüft Abwärtskompatibilität |
5 | Net (Plugin-Channels) · Server-Kontrolle mit UID und Paper-Plugin · Commands mit Tab-Vervollständigung · Setting.text/color/key · Tasks · AddonConfig.forServer · HudElement.text · Draw.texture/image · Events key, mouse, scroll · category, permissions, requires · Absturzberichte · Testing + gradlew runTest · CI-Vorlage |
4 | Events blockBroken, use, sound, title, death, respawn · unload() · Bus · Page · Hot-Reload (.addon reload) · gradlew runClient · Upload mit Freigabe, signierte Downloads · Javadoc |
3 | Tasten (keys(), Keys) · Events world, attack, damage, slot · Ressourcen aus assets/<id>/ · Notify, Draw · AddonConfig.decimal/list/dir · desc in Patches optional · gradlew check |
2 | Events tick, join, leave, chat, send, screen · Lib client:client-api · Gradle-Vorlage |
1 | ready, hud, options, AddonConfig, HudColor, Patches |