This page is currently only available in German.
Diese Dokumentation richtet sich an alle, die am Code von MRsleep arbeiten. Was das Plugin für Server-Admins kann und wie man es einrichtet, steht in der README.
1. Überblick
| Plattform | Paper 26.3 (io.papermc.paper:paper-api:26.3.build.200-stable) |
| Java | 25 (Gradle-Toolchain, options.release = 25) |
| Build | Gradle 9.8.1 über den Wrapper (gradlew) |
| Plugin-Typ | Klassisches Bukkit-Plugin (plugin.yml, JavaPlugin) |
| Externe Abhängigkeiten | keine. Adventure/MiniMessage, Gson und java.net.http sind in Paper bzw. im JDK enthalten |
| Paket | at.marcelr.mrsleep |
2. Projektstruktur
mrsleep/
├─ build.gradle.kts Build, Paper-API, Java 25, Ressourcen-Ersetzung, Modrinth-Upload (Minotaur)
├─ gradle.properties version, mcVersion, paperApi, supportedGameVersions, Projekt-IDs
├─ settings.gradle.kts
├─ gradlew / gradlew.bat Gradle-Wrapper (lädt Gradle 9.8.1 selbst)
├─ CHANGELOG.md Änderungen pro Version (Quelle für Release-Notes)
├─ LICENSE MRsleep Lizenz (source-available)
├─ .editorconfig Formatierung; hält das BOM der config.yml
├─ .github/
│ ├─ workflows/build.yml Build + Tests bei jedem Push/PR, JAR als Artefakt
│ ├─ workflows/release.yml Bei Tag mc*-v*: GitHub-Release + Modrinth-Upload
│ ├─ ISSUE_TEMPLATE/ Vorlagen für Fehlermeldungen und Wünsche
│ └─ dependabot.yml monatliche Updates der GitHub-Actions
├─ docs/DOKUMENTATION.md diese Datei
├─ docs/RELEASES.md Versionsschema, Branches, mehrere Minecraft-Versionen, Release-Ablauf
├─ README.md Doku für Server-Admins
└─ src/
├─ main/java/at/marcelr/mrsleep/
│ ├─ MRSleep.java Hauptklasse: Lebenszyklus, verbindet alle Komponenten
│ ├─ Settings.java unveränderlicher Record mit allen Werten aus config.yml
│ ├─ ConfigUpdater.java Config-Migration, Laden mit BOM-Unterstützung
│ ├─ Messages.java Sprachdateien laden/ergänzen, MiniMessage, Konsole immer Englisch
│ ├─ StatusTexts.java Status-Texte eines Spielers (für GUI und /nosleep info)
│ ├─ SleepDisplay.java Fortschrittsanzeige: Bossbar / Actionbar / Titel
│ ├─ SleepManager.java eigene Schlaflogik (Quote, Nacht überspringen)
│ ├─ NoSleepManager.java Blockaden, Cooldowns, Bypässe, Sperren, data.yml
│ ├─ AdminActions.java Admin-Aktionen, gemeinsam für Befehle und Menü
│ ├─ DiscordWebhook.java asynchrone Discord-Logs
│ ├─ UpdateChecker.java Update-Prüfung (Standard: GitHub Releases, alternativ Modrinth)
│ ├─ SleepListener.java Events (Bett, Join/Quit, TimeSkip, WorldLoad)
│ ├─ command/
│ │ ├─ NoSleepCommand.java /nosleep [nächte|off|info]
│ │ └─ MRSleepCommand.java /mrsleep …
│ ├─ gui/
│ │ ├─ Menu.java Basisklasse (InventoryHolder, Klick-Aktionen, render())
│ │ ├─ MenuListener.java Klick-Routing, Item-Schutz, Live-Aktualisierung
│ │ ├─ MainMenu.java Übersicht (Welt, Köpfe, Filter, Seiten)
│ │ ├─ PlayerMenu.java Aktionen für einen Spieler
│ │ ├─ PlayerHead.java Kopf mit Status-Lore, Sortierung
│ │ └─ Items.java Item-Builder
│ └─ util/
│ ├─ DurationUtil.java "1h30m" ⇄ Millisekunden
│ └─ Versions.java Versionen "mc26.3-v1.2.0": SemVer-Vergleich, MC-Version, Vorabversionen
├─ main/resources/
│ ├─ plugin.yml Befehle und Permissions (${version}, ${mcVersion} werden ersetzt)
│ ├─ config.yml Standard-Config (UTF-8 mit BOM!)
│ ├─ lang/de.yml, lang/en.yml alle Texte (UTF-8 mit BOM, gleiche Schlüssel)
│ └─ build-info.properties Version und Projekt-IDs für den Update-Checker (wird ersetzt)
└─ test/java/…/ JUnit-Tests: DurationUtil, Versions, LanguageFilesTest (de/en gleich)
3. Architektur
┌──────────────┐
│ MRSleep │ onEnable / onDisable / reload
└──────┬───────┘
┌─────────┬─────────┬────────┼─────────┬──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
Settings Messages Discord Update- NoSleepManager SleepManager MenuListener
Webhook Checker (Zustand) (Logik) (GUI-Refresh)
▲ ▲
AdminActions ◄────────┤
▲ ▲ │
MRSleepCommand MainMenu/PlayerMenu SleepListener, NoSleepCommand
MRSleepbesitzt alle Komponenten und reicht sie über Getter weiter (plugin.settings(),plugin.noSleep()…). Es gibt keine statischen Singletons.Settingsist ein Record. Beireload()wird er komplett neu erzeugt, sodass niemand einen halb aktualisierten Zustand sieht.- Zustand (Blockaden, Cooldowns, Bypässe, Sperren) liegt nur im
NoSleepManager. Logik über die Schlafquote liegt nur imSleepManager. AdminActionskapselt jede Admin-Aktion mit Rückmeldung, Benachrichtigung des Spielers, Broadcast und Discord-Log. Befehle und Menü rufen dieselben Methoden auf.- Alles läuft auf dem Main-Thread, mit zwei Ausnahmen: HTTP im
DiscordWebhook(eigener Daemon-Thread) und imUpdateChecker(Bukkit-Async-Task). Ergebnisse, die Spieler betreffen, gehen perrunTaskzurück auf den Main-Thread.
4. Schlaflogik (SleepManager)
Warum eine eigene Logik?
In Vanilla bzw. Paper zählen Spieler mit setSleepingIgnored(true) als schlafend, bleiben aber in der Gesamtzahl. Bei 6 Spielern, 2 ignorierten und 50 % braucht Vanilla ceil(6 × 0.5) = 3 Schlafende, von denen 2 die ignorierten sind. Es reicht also 1 echter Schläfer. Gewünscht sind aber 2 von 4. Dafür gibt es keinen API-Hook, deshalb:
- Vanilla zurückdrängen: In verwalteten Welten wird
GameRules.PLAYERS_SLEEPING_PERCENTAGE = 100gesetzt. Vanilla versucht dann nur zu überspringen, wenn wirklich alle schlafen, und dieser Versuch wird abgebrochen (Punkt 2).⚠️ Nicht 101 verwenden: Bei Werten über 100 gibt
ServerLevel#canSleepThroughNights()falsezurück.ServerPlayer#startSleepInBedschickt dann jedem Spieler beim Hinlegen „Keine noch so lange Rast kann diese Nacht verkürzen“ (sleep.not_possible), und die Spieler stehen wieder auf. Das war der Fehler bis Version 0.9.0. Nebeneffekt von 100: Liegen in einer/nosleep-Nacht wirklich alle im Bett, beendet Vanilla trotzdem Regen und Gewitter (resetWeatherCyclewird unabhängig vom abgebrochenen Event aufgerufen). - Sicherheitsnetz:
SleepListener#onTimeSkipbricht jedesTimeSkipEventmitSkipReason.NIGHT_SKIPin verwalteten Welten ab. - Eigener Check: Ein Task prüft alle 10 Ticks jede verwaltete Welt.
Verwaltete Welten
isManaged(world) gilt für Environment.NORMAL, sofern die Welt nicht in sleep.disabled-worlds steht. In managedWorlds merkt sich das Plugin, wo es die Gamerule geändert hat. So kann es sie beim Deaktivieren oder beim Entfernen einer Welt aus der Verwaltung (Reload) wieder auf sleep.percentage setzen.
Ablauf pro Tick und Welt
housekeeping(world)
├─ abgelaufene Blockaden entfernen → Chat "nosleep-expired" + Discord
└─ Nacht hat begonnen (einmal pro nightKey) und blockiert → Chat/Discord "night-start-blocked"
checkSleep(world)
├─ status(world): counted / exempt / sleeping / deepSleeping / needed
├─ niemand schläft → fertig
├─ blockiert → SleepDisplay#blocked ("display-blocked", nur Schlafende) → fertig
├─ deepSleeping ≥ needed → skipNight()
└─ sonst SleepDisplay#progress ("display-sleeping", alle Spieler der Welt)
(niemand schläft oder Nacht übersprungen → SleepDisplay#hide)
Berechnung
excluded = hasPermission("mrsleep.exempt")
∨ (ignore-spectators ∧ Zuschauermodus)
∨ (respect-sleeping-ignored ∧ isSleepingIgnored())
∨ (ignore-admin-mode ∧ im Admin-Modus von MRadmin)
counted = Spieler der Welt, die nicht ausgeschlossen sind
needed = clamp( max(minimum-sleepers, ceil(counted × percentage / 100)), 1, counted )
deep = schlafende gezählte Spieler mit getSleepTicks() ≥ 100 (wie Vanilla)
skip, wenn counted > 0 ∧ deep ≥ needed ∧ keine aktive Blockade
Ausgeschlossene Spieler zählen weder zu counted noch zu sleeping, auch wenn sie im Bett liegen.
skipNight
setFullTime(fullTime + (24000 − time % 24000))springt auf den nächsten Morgen (Tick 0).- Alle Schlafenden werden mit
wakeup(false)geweckt. - Mit
clear-weatherwerdensetStorm(false)undsetThundering(false)gesetzt. - Danach folgen die Chat-Nachricht
night-skippedund das Discord-Eventnight-skipped.
5. /nosleep (NoSleepManager, NoSleepCommand)
Nacht-Schlüssel
nightKey = fullTime / 24000 (+1, wenn time ≥ 23460)
Eine Nacht gehört zu dem Ingame-Tag, an dessen Abend sie beginnt. Tagsüber zeigt der Schlüssel deshalb auf die kommende Nacht, nachts auf die laufende. Ab 23460 funktionieren Betten nicht mehr, die Nacht ist vorbei und der Schlüssel springt auf den nächsten Tag.
| Konstante | Wert | Bedeutung |
|---|---|---|
NIGHT_START |
12542 | Ab hier kann man bei klarem Wetter schlafen |
NIGHT_END |
23460 | Ab hier kann man nicht mehr schlafen |
Blockaden
Gespeichert als world → (player → Block(startKey, endKey)), mit erhaltener Einfügereihenfolge für die Namensliste.
/nosleep NsetztBlock(key, key + N − 1)und ersetzt eine bestehende eigene Blockade.- Aktiv ist ein Block, solange
currentKey ≤ endKey. - Verbleibende Nächte:
min(endKey − currentKey + 1, endKey − startKey + 1). Die zweite Grenze verhindert zu große Werte, wenn jemand die Zeit per/time setzurückdreht. - Zeigt das Plugin eine einzige Zahl an („noch X Nächte“), ist das die längste aktive Blockade (
NoSleepManager.longest), also die Zahl der Nächte, bis wieder geschlafen werden kann. - Wird die Nacht anders übersprungen, z.B. per
/time set day, läuft die Blockade beim nächsten Schlüsselwechsel normal ab.
Befehlslogik
/nosleep off|aus|cancel → eigene Blockade aufheben (kein Cooldown)
/nosleep (aktiv) → eigene Blockade aufheben (kein Cooldown)
/nosleep (nicht aktiv) → 1 Nacht blockieren ┐ prüft Cooldown; startet ihn sofort, wenn
/nosleep N → N Nächte (1..max-nights) ┘ man schlafen kann, sonst bei Nachtbeginn
Cooldown erst bei Nachtbeginn:
- Jeder
Blockhat ein Flagcharged.setBlocksetzt es sofort (und ruftmarkUsedauf), wennSleepManager.sleepPossible(world)gilt (isNightoder Gewitter). Sonst bleibt der Block „pending“. SleepManager#housekeepingruft alle 10 TicksNoSleepManager#chargePendingauf. Sobald man in der Welt schlafen kann, startet bei allen wartenden Blöcken der Cooldown (lastUse = jetzt), und die Spieler bekommennosleep-cooldown-started.- Hebt der Spieler einen wartenden Block selbst auf (
NoSleepCommand#lift), wurde nie ein Cooldown gesetzt (nosleep-deactivated-free). Bei einem schon belasteten Block läuft der Cooldown weiter (nosleep-deactivated-cooldown). - Blöcke aus
data.ymlvor 0.9.4 haben kein Flag und gelten als belastet.
Der Cooldown entfällt bei mrsleep.nosleep.nocooldown (default: false, gilt also auch nicht automatisch für OPs) oder bei einem aktiven Bypass (bypassUntil > now). Vor der Cooldown-Prüfung wird geprüft, ob eine Sperre besteht. Das Aufheben einer eigenen Blockade bleibt auch gesperrten Spielern möglich.
Sperren
NoSleepManager.Ban(until, reason, by), wobei until == Long.MAX_VALUE dauerhaft bedeutet. AdminActions#ban speichert die Sperre und hebt sofort alle laufenden Blockaden des Spielers auf. Abgelaufene Sperren werden beim Zugriff entfernt.
Persistenz: plugins/MRsleep/data.yml
cooldowns:
<spieler-uuid>: <zeitpunkt letzte Nutzung, epoch ms>
bypass:
<spieler-uuid>: <ablaufzeitpunkt, epoch ms>
bans:
<spieler-uuid>:
until: <epoch ms | 9223372036854775807 = dauerhaft>
reason: <text>
by: <name des admins>
blocks:
<welt-uuid>:
<spieler-uuid>:
start: <nightKey>
end: <nightKey>
Die Datei wird nach jeder Änderung synchron geschrieben (sie ist sehr klein) und zusätzlich in onDisable. Abgelaufene Einträge werden beim Speichern entfernt.
6. Konfiguration laden & migrieren (ConfigUpdater)
- Laden:
MRSleep#getConfig()undreloadConfig()sind überschrieben und nutzenConfigUpdater.load(). Dort wird ein UTF-8-BOM am Dateianfang entfernt, bevor SnakeYAML die Datei liest. Als Defaults dient die mitgelieferteconfig.yml, fehlende Schlüssel greifen also auf sie zurück. - BOM: Die mitgelieferte
config.ymlbeginnt mitEF BB BF. Daran erkennen Windows-Editoren und FTP-Clients die Kodierung, statt Windows-1252 zu raten. Sonst werden Umlaute zu „ä“. ⚠️ Beim Bearbeiten vonsrc/main/resources/config.ymldarauf achten, dass das BOM erhalten bleibt (in VS Code unten rechts „UTF-8 with BOM“). - Migration:
config-versionin der Datei kleiner alsConfigUpdater.CURRENT_VERSION:- Die mitgelieferte
config.ymlwird als Vorlage geladen. Die Kommentare und die Reihenfolge bleiben erhalten, weil Bukkit Kommentare mitliest. - Jeder Wert der alten Datei, dessen Schlüssel es in der Vorlage noch gibt, wird übernommen. Ausnahme sind die
REPLACED_KEYS, also Texte, deren Inhalt sich mit neuen Funktionen geändert hat. - Die alte Datei wird als
config.v<N>.backup.ymlgesichert, die neue mit BOM geschrieben.
- Die mitgelieferte
- v6 (Sprachdateien):
migrateTextsüberträgt vor der Migrationmessages.*,discord.events.*.message,discord.test-messageundgui.ban-reasonaus der alten Config nachlang/de.yml(die alten Texte waren deutsch).REPLACED_KEYSundRENAMED_KEYSgelten dabei weiter. - Wenn sich die Config-Struktur ändert:
CURRENT_VERSIONundconfig-versionin der Vorlage erhöhen. Bei umbenannten Schlüsseln die Werte vor dem Übernehmen umschreiben, bei geänderten Standardtexten den Schlüssel inREPLACED_KEYSaufnehmen.
7. Admin-Menü (gui/*)
Menuist selbst derInventoryHolder.MenuListenererkennt Menüs übergetTopInventory().getHolder(false) instanceof Menu. Jeder Klick in ein Menü wird abgebrochen, ebenso jeder Drag, damit sich keine Items herausnehmen lassen. Danach wird die Aktion des angeklickten Slots ausgeführt.render()leert das Inventar und ruftdraw()neu auf.MenuListenermacht das allegui.refresh-ticksTicks für jedes offene Menü. Da Bukkit Inventaränderungen erst am Tick-Ende synchronisiert, flackert dabei nichts.MainMenu: Oben die Welt (eine Uhr, nachts ein Recovery-Kompass, Klick wechselt die Welt), das Info-Buch (Version und Update-Status) und „Blockaden entfernen“. In der Mitte bis zu 36 Köpfe pro Seite, sortiert nachPlayerHead.rank(Blockierer → Schlafende → Zählende → Ausgenommene, danach alphabetisch) und gefiltert nachFilter. Unten Seiten, Filter, Schließen und Reload.PlayerMenu: Aktionen für einen Spieler. Jede Taste läuft überbutton(...): Sie prüft die Permission (sonst Lore-Hinweis, Ablehnungs-Sound undno-permission), ruft die Methode inAdminActionsauf, spielt den Erfolgs-Sound und zeichnet neu. Geht der Spieler offline, springt das Menü zur Übersicht zurück.- Alle Texte kommen aus
gui-*der Sprachdatei.Messages#itemundMessages#loreentfernen die Kursivschrift, die Minecraft standardmäßig für Item-Texte nutzt. Werte in der Kopf-Lore (z.B.<counts>) sind formatierte Komponenten (Messages.tag(name, Component)).
8. Update-Checker (UpdateChecker)
- Die Projekt-IDs stehen in
gradle.propertiesund landen beim Build inbuild-info.properties. - Modrinth:
GET /v2/project/{id}/version?loaders=["paper"]&include_changelog=false. GitHub:GET /repos/{repo}/releases. Entwürfe werden ignoriert. Vorabversionen werden berücksichtigt, wenninclude-prereleasesan ist oder die installierte Version selbst eine Vorabversion ist (Versions.isPrerelease: 0.x oder Suffix). - Die höchste Version wird mit
Versions.comparebestimmt (SemVer): Zahlen werden numerisch verglichen (0.10.0 > 0.9.2), ein führendesvund Build-Metadaten (+…) werden ignoriert, eine Release-Version gilt als neuer als ihre Vorabversion (1.0.0 > 1.0.0-rc.1), und Vorabversionen werden je Teil verglichen (rc.10 > rc.2). - Der Check läuft beim Start (nach 5 s) und danach alle
interval-hoursStunden als Async-Task, außerdem bei/mrsleep version. Ist eine neuere Version verfügbar, erscheint einmal pro Version ein Hinweisblock in der Konsole, und online-Admins (mrsleep.admin.update) bekommen eine Nachricht. Beim Join kommt der Hinweis nach 3 s. - Fehler wie 404 (Projekt noch nicht veröffentlicht) werden einmal als INFO geloggt, danach erst wieder, wenn sich der Grund ändert.
- Nur dieselbe Minecraft-Version: Releases mit anderem
mc…-Präfix (GitHub) bzw. ohne passendegame_versions(Modrinth) werden übersprungen. Alte Tags ohne Präfix (v1.0.0) zählen mit. Die eigene MC-Version kommt ausbuild-info.properties(mcVersion). - Der User-Agent lautet
MRsleep/<version> (<github-url>). Modrinth verlangt einen eindeutigen User-Agent.
9. Nachrichten (Messages)
- Alle Texte stehen in
lang/<sprache>.yml(mitgeliefert:de,en) und werden mit MiniMessage dargestellt. Die Config enthält nurlanguage. - Laden (
Messages#load): Fehlende mitgelieferte Sprachdateien werden nachplugins/MRsleep/lang/kopiert. Der Sprachcode wird aufgelöst: erst exakter Dateiname, dann Aliase (eng/english→en,deutsch/german→de), dann der Teil vor_/-(de_AT→de). Gibt es die Datei nicht, wirdenverwendet. - Ergänzen: Schlüssel, die in der Datei fehlen, werden aus der mitgelieferten Datei derselben Sprache (sonst
en) eingefügt und die Datei mit BOM zurückgeschrieben. - Discord-Texte stehen unter
discord.<event>bzw.discord.test-messageder Sprachdatei, An/aus und Farbe weiter in der Config (discord.events.<event>). - Konsole immer Englisch: Log-Ausgaben sind fest englisch. Nachrichten an die Konsole (Befehle von der Konsole, auch asynchrone Antworten wie Update- oder Discord-Test) nutzen die mitgelieferte
en.yml:Messages#as(sender, …)setzt dafür ein ThreadLocal,send()tut das automatisch.console(key, …)liefert englische Texte für Logs. - Geänderte Standardtexte: Bestehende Dateien bekommen nur fehlende Schlüssel automatisch. Ändert sich ein vorhandener Text (z.B. neuer Platzhalter),
Messages.LANG_VERSIONundlang-versionin beiden Dateien erhöhen und den Schlüssel inCHANGED_TEXTSeintragen. Ältere Dateien der mitgelieferten Sprachen bekommen ihn dann erneuert (eigene Anpassungen daran gehen verloren), eigene Sprachen bleiben unberührt. - Neue Texte: in beiden Dateien
de.ymlunden.ymlanlegen.LanguageFilesTestprüft beim Build, dass beide dieselben Schlüssel haben und keiner wieyes/noals Boolean gelesen wird. <prefix>wird als parsed Placeholder eingesetzt, darf also selbst Formatierung enthalten. Alle anderen Platzhalter sind unparsed, damit Spielernamen keine Tags einschleusen können.- Eine leere Nachricht (
"") wird nicht gesendet. Damit lässt sich jeder Hinweis abschalten. Ein fehlender Schlüssel erzeugt sichtbarMissing text: <key>. nights(n)erzeugt mitnights-singularundnights-pluralz.B. „1 Nacht“ oder „3 Nächte“.broadcast(world, …)sendet nur an Spieler dieser Welt, denn Blockaden gelten pro Welt.
Wo welche Nachricht ausgelöst wird:
| Schlüssel | Auslöser |
|---|---|
bed-prevented / bed-blocked-warning |
SleepListener#onBedEnter (je nach prevent-bed-enter) |
player-sleeping |
SleepListener#onBedEntered, einen Tick nach dem Hinlegen, nur wenn nicht blockiert und der Spieler zählt |
join-blocked-info |
SleepListener#onJoin |
nosleep-broadcast-quit |
SleepListener#onQuit |
night-start-blocked, nosleep-expired |
SleepManager#housekeeping |
display-* |
SleepDisplay (über SleepManager#checkSleep) |
night-skipped |
SleepManager#skipNight |
player-sleeping (Empfänger) |
SleepListener#onBedEntered, je nach sleep.sleep-message |
nosleep-* |
NoSleepCommand |
bypass-*, cooldown-reset*, cleared*, released, ban-*, unban-*, reloaded |
AdminActions (Befehle und Menü) |
status, debug-*, bans-*, version* |
MRSleepCommand |
update-* |
UpdateChecker |
gui-* |
gui/* |
10. Discord-Webhook (DiscordWebhook)
load(section)liestdiscord.*und prüft die URL. Ist sie leer oder ungültig, bleibt Discord aus. Der Grund wird in der Konsole gewarnt und indisabledReasongehalten. Bei Erfolg steht „Discord webhook active (x/y events enabled)“ in der Konsole.- Events: Die Einträge aus
discord.eventsder eigenen Datei, ergänzt um alle Events aus der mitgelieferten Config, die dort fehlen. Bukkit würde für fehlende Abschnitte sonst stillschweigend leere Sektionen liefern. Ruft der Code ein Event auf, das es trotzdem nicht gibt, kommt einmalig eine Warnung. log(eventKey, key1, value1, key2, value2, …)ersetzt{key}im Event-Text. Die Werte werden für Discord-Markdown escaped (_ * ~ \| > \), damit z.B.Mr_Steve` nicht kursiv wird.- Das JSON entsteht mit Gson: ein Embed mit
description,color,timestampund optionalfooter, oder einfachercontent. Zusätzlich wird immerallowed_mentions: {parse: []}gesetzt, damit nie jemand gepingt wird. - Gesendet wird über
java.net.http.HttpClientauf einem Single-Thread-Executor. Dadurch bleibt die Reihenfolge erhalten, und bei HTTP 429 kann der Threadretry_afterabwarten (höchstens 3 Versuche). - Der Zustand (
Config-Record) istvolatileund wird beim Reload atomar ersetzt. shutdown()wartet inonDisablebis zu 5 Sekunden, damit letzte Logs noch rausgehen.- Diagnose:
send()gibtnull(Erfolg) oder eine Fehlerbeschreibung zurück und merkt sich das Ergebnis inlastResult. Bekannte Statuscodes werden erklärt (400 abgelehnt, 401/403 Token ungültig, 404 Webhook gelöscht)./mrsleep discord testnutzttest(), das mit?wait=truesendet, damit Discord die Zustellung bestätigt. bed-blockedwird überSleepListener#loggedBedAttemptspro Welt, Spieler und Nacht nur einmal gesendet. Das Set wird bei jedemTimeSkipEventgeleert.
11. Permissions-Design
mrsleep.exempthatdefault: false, damit OPs nicht automatisch aus der Quote fallen.mrsleep.adminenthält allemrsleep.admin.*als Kinder, aber nichtmrsleep.exempt.- Bei
/mrsleepsteht in derplugin.ymlbewusst keine Permission. Sonst würde ein Moderator, der nurmrsleep.admin.clearhat, den Befehl gar nicht sehen. Die Prüfung passiert pro Unterbefehl inMRSleepCommand#check. - Bekannte Einschränkung: Die LuckPerms-Wildcards
*undmrsleep.*schließenmrsleep.exemptmit ein. Abhilfe steht in der README.
12. Entwicklung
Voraussetzungen (bereits eingerichtet)
- Eclipse Temurin JDK 25 (
C:\Program Files\Eclipse Adoptium\jdk-25…,JAVA_HOMEist systemweit gesetzt) - VS Code: Extension Pack for Java und Gradle for Java
.vscode/settings.json(im Workspace-Rootc:\REPO\mc) legt das JDK für Java- und Gradle-Extension fest und aktiviert verschachtelte Gradle-Projekte.vscode/tasks.json: „MRsleep: Build“ (Standard-Build-Task,Strg+Shift+B) und „MRsleep: Clean Build“
Bauen
cd mrsleep
gradlew.bat build
→ build/libs/MRsleep-mc<mcVersion>-v<version>.jar
Die Versionen stehen in gradle.properties und werden beim Build in die plugin.yml (${version}, ${mcVersion} als api-version) und in die build-info.properties übernommen.
Tests
gradlew.bat test nur die Tests
gradlew.bat build Kompilieren + Tests + JAR (schlägt fehl, wenn ein Test fehlschlägt)
- JUnit 6 (
src/test/java). Getestet wird die Logik, die ohne laufenden Server auskommt:DurationUtil(Parsen und Formatieren von Zeitangaben),Versions(SemVer-Vergleich, MC-Präfix, Vorabversionen) undLanguageFilesTest(de.yml und en.yml haben dieselben Schlüssel, keine Schlüssel wieyes/no). - Alles, was Bukkit braucht (Schlafen, Events, GUI), prüft die manuelle Testcheckliste weiter unten auf dem Testserver.
- Die Ergebnisse stehen in
build/reports/tests/test/index.html. Auf GitHub laufen die Tests bei jedem Push (build.yml) und vor jedem Release. - Neue Logik ohne Server-Bezug am besten nach
util/legen und dort testen.
Versionierung & Release
Ausführlich in RELEASES.md (Versionsschema, Branches, mehrere Minecraft-Versionen, Checklisten). Kurz:
- Version = Minecraft-Version + Plugin-Version:
mc26.3-v1.2.0. Ingradle.propertiesstehenversion(SemVer: MAJOR = große Releases, MINOR = Features, PATCH = Hotfixes) undmcVersiongetrennt. Git-Tag, JAR-Name und Modrinth-Version nutzenmc26.3-v1.2.0, dieplugin.yml1.2.0+mc26.3(Paper setzt selbst ein „v“ davor). - Vorabversionen:
0.xoder Suffix wie-rc.1. Auf Modrinth als „beta“, auf GitHub als Pre-Release (isPrereleaseinbuild.gradle.kts,PRERELEASEinrelease.yml). - Branches:
beta= Entwicklung (neueste MC-Version),main= stabil (neueste MC-Version),mc/<version>= Wartung älterer MC-Versionen. Alle werden bei jedem Push gebaut (build.yml). release.ymlstartet bei Tagsmc*-v*, prüft Tag gegengradle.properties, lässt stabile Versionen nur vonmainodermc/<mcVersion>zu, baut, liest den Abschnitt## [<version>]ausCHANGELOG.md, erstellt das GitHub-Release (nur vonmainals „Latest“) und lädt auf Modrinth hoch (SecretMODRINTH_TOKEN).- Update-Checker: vergleicht nur Versionen derselben Minecraft-Version (
Versions.minecraft). Alte Tags ohne Präfix (v1.0.0) zählen als 26.3. - Einmalig einrichten:
- GitHub-Repository anlegen (Name wie
githubRepositoryingradle.properties) und pushen. - Auf Modrinth ein Projekt mit der Slug aus
modrinthProjectIdanlegen. - Auf Modrinth unter Settings → PATs ein Token mit dem Recht „Create versions“ erzeugen und auf GitHub unter Settings → Secrets and variables → Actions als
MODRINTH_TOKENhinterlegen.
- GitHub-Repository anlegen (Name wie
Manuelle Testcheckliste
| # | Test | Erwartung |
|---|---|---|
| 1 | Start, /gamerule players_sleeping_percentage |
100 in der Oberwelt |
| 1b | Hinlegen | keine Meldung „Keine noch so lange Rast …“, sofort Bossbar 1/x Spieler schlafen mit Fortschrittsbalken, auch bei offenem Bett-Chat sichtbar |
| 1c | Spieler mit mrsleep.exempt legt sich hin |
Hinweis bed-excluded mit Grund |
| 1d | /mrsleep debug |
Pro Welt Gamerule/Zeit/Blockade, pro Spieler „zählt“ und „schläft (Ticks)“ |
| 2 | 4 Spieler, 1 mit mrsleep.exempt, 50 % |
2 von 3 müssen schlafen; Bossbar zeigt x/2 |
| 3 | Ausgenommener Spieler schläft allein | Nacht wird nicht übersprungen |
| 4 | /nosleep tagsüber, dann nachts hinlegen |
bed-blocked-warning, kein Skip |
| 5 | /nosleep erneut |
Blockade aufgehoben, kein neuer Cooldown |
| 6 | /nosleep 3, dann 3 Nächte per /time set durchspielen |
„noch 3/2/1 Nächte“, danach nosleep-expired |
| 7 | /nosleep 4 bei max-nights: 3 |
nosleep-invalid-nights |
| 8 | Nachts /nosleep, aufheben, erneut /nosleep |
nosleep-deactivated-cooldown, danach Cooldown-Meldung |
| 8b | Tagsüber /nosleep (Hinweis nosleep-pending-info), vor Nachtbeginn aufheben, erneut /nosleep |
nosleep-deactivated-free, erneutes Blockieren klappt sofort |
| 8c | Tagsüber /nosleep, dann /time set 13000 |
nosleep-cooldown-started; /mrsleep debug zeigt vorher „startet bei Nachtbeginn“, danach „noch 15m“ |
| 9 | /mrsleep bypass <spieler> 10m, dann wie 8 |
kein Cooldown, Spieler wird benachrichtigt |
| 10 | Blockierender Spieler geht offline | nosleep-broadcast-quit, Schlafen wieder möglich |
| 11 | Discord-URL eintragen, /mrsleep reload |
Event admin-reload erscheint im Kanal |
| 12 | Plugin entfernen und Server neu starten | Gamerule steht wieder auf sleep.percentage |
| 13 | Config und Daten von 1.0.0, Start mit 1.1.0 | Log „Moved … texts … to lang/de.yml“ und „Updated config.yml from version 5 to 6“, eigene Texte stehen in lang/de.yml, config.v5.backup.yml vorhanden, Dateien beginnen mit BOM |
| 14 | /mrsleep ban <spieler> 2h Test, dann /nosleep als dieser Spieler |
nosleep-banned, die laufende Blockade wurde aufgehoben |
| 15 | /mrsleep gui |
Köpfe aktualisieren sich live (Schlafsekunden zählen hoch), Filter und Seiten funktionieren, Items lassen sich nicht herausnehmen |
| 16 | Spieler-Menü ohne mrsleep.admin.ban |
Sperr-Buttons zeigen „Keine Berechtigung“, ein Klick wird abgelehnt |
| 17 | /mrsleep version |
MRsleep v<version> (MC 26.3), Links und Update-Status |
| 18 | language: en, /mrsleep reload |
Chat, Bossbar, Menü und Discord-Texte auf Englisch |
| 19 | language: eng bzw. language: xx |
eng wird zu en; bei xx Warnung in der Konsole und Englisch |
| 20 | Text in lang/de.yml ändern, /mrsleep reload |
geänderter Text erscheint; nach Neustart bleibt er erhalten |
| 21 | Befehle von der Konsole bei language: de |
Antworten und Logs auf Englisch |
13. Erweiterungsideen
- Eine Permission pro Nächtezahl (z.B.
mrsleep.nosleep.nights.5), ummax-nightsje Gruppe zu staffeln - PlaceholderAPI-Expansion (
%mrsleep_needed%,%mrsleep_blocked%) - Sanftes Vorspulen der Zeit statt eines Sprungs
- Ein eigener Cooldown für das Aufheben, falls
/nosleepals Spam-Werkzeug missbraucht wird
Source: mrsleep/docs/DOKUMENTATION.md ↗