MC Launcher

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:

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.

Kein Mod-Loader nötig. Addons laufen im Client des Launchers, nicht über Fabric oder Forge. Minecraft-Klassen heißen so wie im offiziellen Jar, z. B. 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
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

module.json

FeldPflichtBedeutung
namejaAnzeigename im Katalog und als Reiter unter Einstellungen → Addons.
developerjaDein Name oder Team.
versionjaBei jeder Änderung erhöhen – nur so sehen Spieler ein Update.
minecraftjaMinecraft-Version, gegen die das Addon gebaut ist, z. B. 26.3.
descriptionjaBeschreibung für Katalog und Website. Klartext, keine Formatierung.
apineinAPI-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.
mainnur VorlageDeine Module-Klasse; die Gradle-Vorlage erzeugt daraus die Service-Datei.
categoryneinEine von hud, pvp, minimap, visual, utility, chat, performance, other (Standard). Server können ganze Kategorien abschalten – ehrlich wählen, sonst gibt es keine Freigabe.
permissionsneinWas dein Addon darf, siehe Berechtigungen. Spieler sehen die Liste vor dem Installieren.
requiresneinIDs anderer Addons aus dem Katalog, z. B. ["minimap"]. Werden mitinstalliert und vorher geladen. Verständigt euch über den Bus.
hooksbei PatchesKlasse mit den Hook-Methoden, als interner Name, z. B. meinaddon/Hooks.
patchesneinEingriffe in Minecraft-Methoden, siehe Patches.

Lebenszyklus

Wann dein Code läuft, hängt davon ab, was das Addon braucht:

1
Vor Minecraft – Patches

Die Patches aus der module.json baut der Loader ein, bevor Minecrafts Klassen laden. Greifen nach dem Installieren erst ab dem nächsten Spielstart.

2
Client-Start – Konstruktor

Minecraft ist erstellt (Fenster, Optionen, Spielordner), Texturen und Übersetzungen laden noch.

3
Spiel geladen – 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

BerechtigungBedeutet
chatEvents chat und send – Chat lesen, ausblenden, Eingaben abfangen; ab 6.2.0 auch Chat.onReceive (filtern, umschreiben)
inputEvents key, mouse, scroll
serverNet – Daten mit dem Server tauschen
networkWeb (ab 6.0.1 geprüft); eigene Verbindungen ins Internet
filesDateien außerhalb von client-modules/<id>/ (nur angezeigt)
friendsFriends – 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"));
}
KlasseWofür
RenderNur in world: box, line, text3d, item3d, beam
GhostBlocksBlöcke nur für dich (place), eigene Modelle aus Blockbench-.glb (load + model, Animation „idle“)
ParticlesVanilla-Partikel (spawn), eigene mit Textur, Farbverlauf und Größe (Spec, custom), Quellen an Stellen oder Wesen (emitter)
Highlight, BlockTintLeucht-Umriss für Wesen, Rahmen um Blöcke; Blöcke im Umkreis einfärben (nur sichtbare Seiten)
DecalsTextur auf eine Blockseite kleben
Sky, Weather, PostFxHimmel, Nebel, Wolken umfärben; Wetter nur optisch; Vollbild-Shader (Vanilla oder assets/<id>/post_effect/)
Entities, Trails, LightWesen 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“
KlasseWofür
EventsonAttack (true = abbrechen), onBlockBreak, onDeath, onDimensionChange, onJoin, onLeave – wie die Module-Methoden, als Lambda; jedes liefert ein Tasks.Handle zum Abmelden
Chat.onReceiveEingehende Nachrichten: null = ausblenden, andere Nachricht = statt dessen zeigen. Braucht chat
Inventoryfind(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
WaypointsGemeinsame Wegpunkte aller Addons je Server/Welt: add, remove (nur eigene), all, dimension
Cameraoffset(vec3), zoom(faktor) (dritte Person), freelook(an) (Server-Funktion "freelook") – je Addon, wie View.fov
Screenshotscapture(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.arrowKompassleiste 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
}

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:

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

FeldBedeutung
idEindeutig ü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).
nameAnzeigename im HUD-Editor.
ax, ayAnker als Bruchteil des Bildschirms: 0 = links/oben, 0.5 = Mitte, 1 = rechts/unten.
ox, oyVersatz der linken oberen Ecke vom Anker, in GUI-Pixeln.
w, hGröß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.

BausteinDarstellung
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));
}
Neu abgefragt: 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
Heiße Pfade: Die Getter parsen bei jedem Aufruf. Was jedes Bild gebraucht wird (z. B. in 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))

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

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

Abwärtskompatibel: Was einmal in der API ist, bleibt. Der Build des Clients prüft jede neue Version gegen alle älteren (Klassen, Methoden, Signaturen) und bricht bei einem Bruch ab. Ab welcher Version es etwas gibt, steht als @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

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
    }
  ]
}
FeldBedeutung
clsZielklasse als interner Name: / statt ., innere Klassen mit $.
methodMethodenname, <init> für Konstruktoren.
descExakter 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“.
hookName der public static-Methode in deiner hooks-Klasse.
replacetrue = 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

Patches greifen erst nach einem Neustart. Hooks laufen mitten in Minecraft, oft jedes Bild und teils auf Hintergrund-Threads. Halte sie schnell, lege dort keine Objekte an und fang Fehler ab: lieber die eigene Funktion abschalten, als das Spiel abstürzen zu lassen.

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:

IDEKonfigurationenDebuggen
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 / Terminalgradlew runClientgradlew 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

Veröffentlichen

  1. version in der module.json erhöhen.
  2. 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 unter assets/<id>/) und stellt es in die Warteschlange.
  3. 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
Sicherheit: Addons laufen mit allen Rechten des Spiels – schau dir Code vor der Freigabe an. Jeder Download ist mit dem Schlüssel des Servers signiert (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

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.

APINeu
6.2.1Freunde & 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.0Welt-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.2Eigenes 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.1Korrekturen, 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.0View.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.1Semver-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
6ServerInfo (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
5Net (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
4Events blockBroken, use, sound, title, death, respawn · unload() · Bus · Page · Hot-Reload (.addon reload) · gradlew runClient · Upload mit Freigabe, signierte Downloads · Javadoc
3Tasten (keys(), Keys) · Events world, attack, damage, slot · Ressourcen aus assets/<id>/ · Notify, Draw · AddonConfig.decimal/list/dir · desc in Patches optional · gradlew check
2Events tick, join, leave, chat, send, screen · Lib client:client-api · Gradle-Vorlage
1ready, hud, options, AddonConfig, HudColor, Patches