Diese Dokumentation richtet sich an alle, die am Code von MRchunks arbeiten. Was das Plugin für Server-Admins kann, steht in der README. Build, Config-Migration, Discord, Update-Checker, Menü-Grundgerüst und Release-Ablauf stammen aus MRsleep und funktionieren genauso. Die dortige Projektdoku beschreibt sie ausführlicher.
1. Überblick
| Plattform | Paper 26.3 (paper-api:26.3.build.200-stable), Java 25 |
| Build | Gradle 9.8.1 (Wrapper), JUnit 6, Minotaur für Modrinth |
| Paket | at.marcelr.mrchunks |
| Abhängigkeiten | keine Pflicht. Optional squaremap (compileOnly squaremap-api, softdepend) |
2. Projektstruktur
src/main/java/at/marcelr/mrchunks/
├─ MRChunks.java Hauptklasse: Lebenszyklus, Prüf-Task, verbindet alles
├─ Settings.java Record mit allen Config-Werten (validiert)
├─ ConfigUpdater.java Config laden (BOM) und migrieren ┐
├─ Messages.java Sprachdateien lang/*.yml (MiniMessage) │ aus MRsleep
├─ DiscordWebhook.java asynchrone Discord-Logs mit Diagnose │ übernommen
├─ UpdateChecker.java GitHub/Modrinth-Updateprüfung ┘
├─ Loader.java ein gesetzter Chunkloader
├─ LoaderManager.java Loader, Chunk-Tickets, Limits, Sperren, data.yml
├─ LoaderItem.java das markierte Chunkloader-Item
├─ LoaderListener.java Setzen, Unzerstörbarkeit, Rechtsklick, Welt laden
├─ BorderDisplay.java Grenze als leuchtende Block-Displays (nur für den Betrachter)
├─ AdminActions.java alle Aktionen (Befehle und Menüs nutzen dieselben)
├─ Texts.java gemeinsame Platzhalter (<name>, <number>, <world>, <x> …)
├─ command/ /chunkloader, /mrchunks
├─ dummy/ Dummies (Dummy-Spieler: Lebenszyklus, Events, Skins), DummyBackend (Schnittstelle)
│ └─ nms/ NmsDummyBackend, FakeConnection - einzige Stelle mit Server-Interna
├─ link/ LinkRequests (Anfrage + Zustimmungs-Dialog für verknüpfte Accounts)
├─ gui/ Menu, MenuListener, Items, Heads, Pages, LoaderIcons, PlayerIcons,
│ LoaderMenu, MyLoadersMenu, AdminMenu, PlayerLoadersMenu, LinkMenu, LinkPickerMenu
├─ map/ MapHooks (ohne squaremap-Typen), SquaremapHook (Ebene, Marker, Icon,
│ Raster, Spieler ausblenden), SquaremapSetup (squaremap-Config einrichten)
└─ util/ DurationUtil, Versions (mit Tests)
src/main/resources/
├─ config.yml UTF-8 mit BOM!
├─ plugin.yml, build-info.properties
└─ web/squaremap-grid.js Raster + ausgeblendete Spielerliste für squaremap
3. Chunks laden (LoaderManager)
Tickets
ticket(Standard):World#addPluginChunkTicket. In 26.3 heißt das Ticket-TypPLUGIN_TICKETauf Level 31 (Entity-Ticking) mit den Flags „lädt“ und „simuliert“. Es wird an das Plugin gebunden und beim Deaktivieren entfernt.forceload:World#setChunkForceLoaded, wie/forceload. Der Ticket-TypFORCEDhat zusätzlich keep dimension active. Das ist nur nötig, wenn in der Paper-Configunsupported-settings.disable-world-ticking-when-emptyaktiv ist (sonst gehtServerLevelnach 300 leeren Ticks schlafen). MRchunks merkt sich inforcedByUs, welche Chunks es selbst geforceloadet hat. Chunks, die vorher schon geforceloadet waren, werden nie entladen.
Überschneidungen
coverage: Welt → Chunk-Schlüssel → Anzahl aktiver Loader. Ein Ticket entsteht beim Wechsel von 0 auf 1 und verschwindet beim Wechsel von 1 auf 0. Radius und Lademethode, mit denen die Tickets erstellt wurden (appliedRadius, appliedMethod), werden gespeichert. Deshalb löst reload() zuerst alle Tickets mit den alten Werten (releaseAll) und legt sie danach mit den neuen an (applyAll).
Was läuft
Laut dekompiliertem ServerChunkCache ticken alle Entity-Ticking-Chunks, auch mit Zufalls-Ticks. Mob-Spawning läuft dagegen nur in spawningChunks, also in der Nähe von Spielern. Dazu kommt Papers Entity Activation Range: Mobs ohne Spieler in der Nähe bekommen nur seltene, reduzierte Ticks (keine KI, kaum Bewegung, keine Portale, siehe Issue #12). Beides lösen die Dummy-Spieler (Abschnitt 10).
Welten
Loader in einer Welt, die noch nicht geladen ist, bekommen ihre Tickets per WorldLoadEvent → applyWorld.
4. Schutz (LoaderListener)
| Ereignis | Reaktion |
|---|---|
BlockPlaceEvent (HIGH) |
Prüfung von Permission, Welt, Sperre, Limit und Überschneidung (loaders.overlap: deny-own/deny-all). Bei Erfolg folgt die Registrierung erst bei MONITOR, wenn kein Schutz-Plugin abgebrochen hat |
BlockBreakEvent |
abgebrochen, mit Hinweis |
EntityExplodeEvent, BlockExplodeEvent |
Loader-Blöcke werden aus der Blockliste entfernt |
BlockPistonExtend/RetractEvent |
abgebrochen, wenn ein Loader bewegt würde |
EntityChangeBlockEvent, BlockBurnEvent |
abgebrochen (Wither, Endermen, Feuer bei brennbarem Material) |
PrepareItemCraftEvent |
kein Ergebnis, wenn ein Loader-Item im Raster liegt (verhindert Emeraldblock → 9 Smaragde) |
Prüf-Task (check-interval-seconds) |
validate() stellt fehlende Blöcke wieder her oder löscht den Loader |
ChunkLoadEvent |
Loader im geladenen Chunk werden einen Tick später geprüft (validate(Collection)), auch deaktivierte, deren Chunks sonst nie geladen sind |
Die Block-Suche läuft über einen Positions-Index (byPosition), damit große Explosionen nicht alle Loader durchgehen müssen.
Überschneidung: Zwei Bereiche überschneiden sich, wenn die Mittel-Chunks auf beiden Achsen höchstens 2 × radius auseinander liegen (LoaderManager#overlapping). Tickets laufen über den Referenzzähler pro Chunk (coverage), deshalb ist Überlappen technisch immer sauber. loaders.overlap regelt nur, ob es erlaubt ist: allow, warn (Hinweis mit sharedChunks und Grenze nach dem Setzen), deny-own, deny-all.
Namen & Nummern: Loader#id ist intern (Schlüssel in data.yml, Marker-Keys). Spieler sehen Loader#number, die kleinste freie Nummer des Owners (nextNumber), oder den Namen aus dem Umbenennen-Dialog (gui/RenameDialog, Paper-Dialog-API mit Textfeld; AdminActions#rename entfernt Steuerzeichen und kürzt auf gui.max-name-length). Ältere data.yml ohne number werden beim Laden in Erstellungsreihenfolge durchnummeriert (assignMissingNumbers). Admin-Befehle nehmen <spieler> <nr>.
Grenze (BorderDisplay): pro Betrachter eine Sitzung mit BlockDisplay-Entities: waagrechte Balken je Chunk der Außenkante auf mehreren Höhen und Pfosten an jeder Chunk-Ecke. Die Entities sind persistent = false (werden nie gespeichert) und visibleByDefault = false (nur player.showEntity macht sie sichtbar), leuchten (setGlowing, Farbe aus border.color) und sind über ihre Mitte verankert, damit der Client sie lange anzeigt. Alle 10 Ticks folgen sie der Höhe des Spielers (setTeleportDuration glättet), fehlende Teile (Chunk entladen) werden neu erzeugt. Ende: Zeit abgelaufen, Button im Menü, Loader gelöscht (loaderRemoved), Quit, Weltwechsel, Plugin-Stopp. Beim An/Ausschalten wird sie neu eingefärbt (loaderChanged).
5. Item (LoaderItem)
Material, Name, Lore und Glanz kommen aus der Config. Markiert wird das Item im PersistentDataContainer (mrchunks:chunkloader). Zwei Limits (LoaderManager.LimitKind PLACE/ACTIVE), beide aufgelöst nach Admin-Override → höchste Permission mrchunks.limit.<n> bzw. mrchunks.active.<n> (nur online) → Config-Standard. Neue Loader starten nur aktiv, wenn canActivate gilt. AdminActions#toggle lehnt Aktivieren über dem Limit ab (außer mit mrchunks.admin.manage). enforceActiveLimit deaktiviert beim Senken die neuesten aktiven Loader. In data.yml stehen die Overrides unter limits bzw. active-limits.
Limit-Logik: Setzen erlaubt, wenn gesetzte < Limit. give gibt höchstens Limit − gesetzte − Items im Inventar aus. Damit kann niemand Items horten, um das Limit zu umgehen, denn beim Setzen wird immer neu gezählt.
6. Daten (plugins/MRchunks/data.yml)
next-id: 3
loaders:
'1': { owner: <uuid>, world: <uuid oder Weltname>, world-name: world, x: 100, y: 80, z: 100, created: <ms>, number: 1, name: "Farm", active: true, dummy: true }
limits:
<spieler-uuid>: 10
active-limits: { <spieler-uuid>: 3 }
dummy-limits: { <spieler-uuid>: 2 }
# Limits aus Permissions vom letzten Mal online (place | active | dummy)
known-limits: { place: { <spieler-uuid>: 15 } }
# Verknüpfte Accounts: Zweit-Account -> Haupt-Account
links:
<zweit-uuid>: <haupt-uuid>
bans:
<spieler-uuid>: { until: <ms | Long.MAX = dauerhaft>, reason: "...", by: "Admin" }
world darf in einer von Hand bearbeiteten Datei auch ein Weltname sein. Beim nächsten Speichern wird daraus die UUID.
7. Menüs (gui/*)
LoaderMenu: Reihe 2 = An/Aus, Mitglieder, Grenze anzeigen/ausblenden, Umbenennen, Löschen; Teleport nur mitmrchunks.admin.teleportunten rechts (Schließen rückt dann nach links). Löschen mit zweitem Klick innerhalb vongui.confirm-seconds. Wird der Loader inzwischen gelöscht, springt das Menü zurück.MyLoadersMenu: die eigenen Loader.AdminMenu: Statistik, Info, Filter, Spielerköpfe (auch offline; das Limit offline kennt nur Admin-Limit und Standard, weil Permissions nur bei Online-Spielern abfragbar sind).PlayerLoadersMenu: Limit (links +1, rechts −1, Shift zurücksetzen), alle löschen, Sperren und die Loader des Spielers.- Alle Aktionen laufen über
AdminActions, das jeweils selbst die Permission prüft.
8. squaremap (map/MapHooks.java, map/SquaremapHook.java)
compileOnly("xyz.jpenilla:squaremap-api:1.4.3")undsoftdepend: [squaremap]. Dadurch startet squaremap vorher, ist aber nicht Pflicht.MapHooksenthält keinen einzigen squaremap-Typ. Erst wennisPluginEnabled("squaremap")gilt, wirdSquaremapHookerzeugt (LinkageErrorwird abgefangen). So entsteht ohne squaremap keinNoClassDefFoundError.SquaremapHookregistriert pro Welt eineSimpleLayerProvider-Ebenemrchunks_loaders(Name ausmap-layer-nameder Sprachdatei) und ein selbst gezeichnetes 16×16-Iconmrchunks_loader. Jeder Loader bekommt einMarker.rectanglein Blockkoordinaten der vollen Chunks und optionalMarker.icon. Tooltips sind HTML, die Werte werden escaped.Aktualisierung:
LoaderManagerruft nach jeder Änderung (setzen, löschen, an/aus)maps().refreshSoon()auf. Mehrere Änderungen in einem Tick führen zu einem einzigen Update. Zusätzlich gleicht ein Task allesquaremap.update-secondsab. Fehlt die Ebene, etwa nach/squaremap reload, wird sie neu registriert. Bei jedem Update werden alle Marker der Welt neu aufgebaut, das ist einfach und driftet nie.Ohne squaremap gibt es keine Karte. Die frühere eigene Webseite (Port 8130) ist entfallen.
Adresse (
util/MapAddress, ohne Bukkit-Bezug, mit Tests):squaremap.addressmit Schema wird unverändert genutzt, ohne Schema ist es ein Host (https://host/mitreverse-proxy, sonsthttp://host:<internal-webserver.port>/). Leer: squaremapsweb-address, falls nichthttp://localhost:8080, sonstBukkit.getIp(), sonst die öffentliche IP, dieMapHooks#detectPublicIpeinmal asynchron voncheckip.amazonaws.comholt (detect-public-ip). Das Grid-Skript wird relativ eingebunden und funktioniert hinter Proxys und unter Unterpfaden.Automatische Einrichtung (
SquaremapSetup): Beim Start schreibt MRchunks, wenn squaremap aktiv ist undsquaremap.auto-configurean ist, einmal proSquaremapSetup.VERSIONdie empfohlenen Werte inplugins/squaremap/config.yml:background.interval-seconds: 5,player-tracker.enabled: false,ui.sidebar.pinned: pinned- pro Welt
world-settings.<welt-key>.map.display-name(Overworld/Nether/End ausmap-dimension-*der Sprachdatei) - im Nether
iterate-up: trueundmax-height: 127
Danach folgt
squaremap reloadper Konsole und nach 2 s ein Abgleich, der Ebene, Icon und Raster wieder herstellt, denn squaremap verwirft sie beim Reload. Erledigt ist erledigt:plugins/MRchunks/squaremap-setup.ymlmerkt sich die Version, spätere Änderungen des Admins bleiben also erhalten./mrchunks map setuperzwingt einen neuen Lauf. Ändern sich die empfohlenen Werte,VERSIONerhöhen.Spieler ausblenden (
squaremap.hide-players):PlayerManager#hidden(uuid, true, false)für alle Online-Spieler, bei jedem Join und bei jedem Abgleich. Damit landen keine Namen inplayers.json. Zusätzlich blendet das Skript die Spielerliste der Seitenleiste per CSS aus (fieldset#players). Beim Deaktivieren werden die Spieler wieder eingeblendet.Chunkraster: squaremap legt seine Leaflet-Karte nicht global ab, nur
window.L.SquaremapHook#installGridschreibtweb/squaremap-grid.js(mit den Einstellungen als JSON von Gson, damit nichts aus der Config Skript einschleusen kann) alsmrchunks-grid.jsinapi.webDir(). Inindex.htmlkommt eine Zeile mit<!-- mrchunks-grid -->vor dem Modul-Skript von squaremap.- Das Skript fängt per
Object.defineProperty(window, "L")die Zuweisung von Leaflet ab und registriertL.Map.addInitHook, noch bevor squaremap die Karte erzeugt. - Zusätzlich fängt
L.Control.Layers.addInitHookdas Ebenen-Menü von squaremap ab. Das Raster wird dort peraddOverlayeingetragen (id = "mrchunks_grid",order = 1000), und squaremap merkt sich die Sichtbarkeit selbst alslocalStorage hide_mrchunks_grid. Findet das Skript nach etwa 10 s kein Menü, zeigt es als Notlösung einen eigenen Schalter. - Das Raster ist ein
L.GridLayermit Canvas-Kacheln in einem eigenen Pane (z-index 350). Blöcke pro Pixel =2^(maxNativeZoom − zoom), wobeimaxNativeZoomaus squaremaps Kachel-Ebene kommt. Neu gezeichnet wird nur, wenn sich die Zoomstufe der Welt ändert. - Die Einbindung ist idempotent und läuft bei jedem Abgleich, weil squaremap seine Web-Dateien beim (Re)Load neu schreibt.
remove()entfernt Zeile und Datei wieder. - Ein
?v=<hash>am Skript-Link sorgt dafür, dass Browser nach Config-Änderungen die neue Version laden.
- Das Skript fängt per
Live: Die Aktualität bestimmt squaremap selbst.
background.interval-secondssetzt sowohl den Render-Takt als auchtiles_update_intervalder Webseite. Empfohlen sind 5 s, siehe README.Getestet mit squaremap 1.4.3 für Paper 26.3: Die Ebene erscheint in
tiles/minecraft_overworld/markers.jsonmit"id":"mrchunks_loaders".
9. Entwicklung
gradlew.bat build Kompilieren + Tests + JAR → build/libs/MRchunks-mc<minecraft>-v<version>.jar
Der Release-Ablauf entspricht MRsleep, siehe RELEASES.md: Versionen heißen mc<minecraft>-v<version> (z.B. mc26.3-v0.2.0), entwickelt wird auf beta, stabile Versionen kommen von main oder einem Wartungs-Branch mc/<version>, und release.yml lädt auf GitHub und Modrinth hoch (Secret MODRINTH_TOKEN). Die Konsole ist immer englisch (Messages#as, Messages#console), Spieler bekommen die Sprache aus language.
Manuelle Testcheckliste
| # | Test | Erwartung |
|---|---|---|
| 1 | /chunkloader give, platzieren |
Meldung „Chunkloader #1 gesetzt – 49 Chunks“, Grenze per Rechtsklick sichtbar |
| 2 | Abbauen, TNT daneben, Kolben dagegen | Block bleibt |
| 3 | Limit erreicht, /chunkloader give |
give-none-left. Weiteres Setzen über gehortete Items geht nicht (place-limit) |
| 4 | Loader-Item in die Werkbank | kein Ergebnis |
| 5 | Menü → Deaktivieren, /forceload query (bei forceload) |
Chunks frei, Block bleibt |
| 6 | Zwei überlappende Loader, einen löschen | gemeinsame Chunks bleiben geladen |
| 7 | Loader im Nether | Bereich geladen (execute in minecraft:the_nether if loaded …) |
| 8 | /mrchunks ban <spieler> |
alle seine Loader weg, Items aus dem Inventar entfernt, give verweigert |
| 9 | /mrchunks limit <spieler> 5 |
Kopf im Admin-Menü zeigt „5 (individuell)“ |
| 10 | Erster Start mit squaremap | Log „squaremap eingerichtet: …“, Dimensionsnamen in tiles/settings.json, players.json leer |
| 11 | /mrchunks reload nach Änderung von radius |
alte Tickets weg, neue Bereichsgröße aktiv |
Einladungen & Ausschluss
- Daten:
LoaderManagerhältloaderMembers(Loader-ID → Spieler → gültig bis) undownerMembers(Owner → Spieler → gültig bis; gilt für alle Loader des Owners, auch künftige).Long.MAX_VALUE= dauerhaft. Indata.ymluntermembers.loaders.<id>.<uuid>bzw.members.owners.<owner>.<uuid>. Abgelaufene Einträge fallen beim Lesen (members) und beim Speichern weg, die eines gelöschten Loaders mit dem Loader. - Rechte (
AdminActions):canToggle=canManage(Owner/Admin) oderisMember. Umbenennen, Löschen undcanManageInvitesbleiben Owner (mrchunks.invite) und Admins (mrchunks.admin.manage) vorbehalten. Beim Aktivieren durch Eingeladene gilt das Aktiv-Limit des Owners, der Owner wird benachrichtigt. Eingeladene dürfen das Menü öffnen (canView). - Menüs:
InvitePickerMenu(Köpfe der Online-Spieler, Dauer per Uhr wählbar, „Namen eingeben“ öffnetInviteDialog),MembersMenu(Liste, Klick = entfernen, bei einem Loader werden die Einladungen für alle Loader grau mit angezeigt),InviteDialog(Paper-Dialog mit Namensfeld und Dauer-Auswahl ausinvites.durations),SharedLoadersMenu(„Mit mir geteilt“). Einstieg: Button Mitglieder imLoaderMenu, Mitglieder für alle Loader und Mit mir geteilt inMyLoadersMenu. mrchunks.exempt(streng): blockiertgive, Setzen, Einladen des Spielers und seine Rechte als Eingeladener.enforceExemptlöscht beim Join und bei/mrchunks reloadseine Loader und Items. Permissions sind nur online bekannt, Offline-Spieler werden beim nächsten Join bereinigt.
10. Dummy-Spieler (dummy/*)
- Warum Server-Interna: Paper hat keine API für künstliche Spieler.
NmsDummyBackend(Paketdummy.nms, gebaut mit paperweight-userdev gegen das Dev-Bundle auspaperApi) erzeugt einen echtenServerPlayerund loggt ihn überPlayerList.placeNewPlayermit einerFakeConnectionein (offenerEmbeddedChannel, verwirft jedes Paket). Abmelden wie bei einem Disconnect:ServerPlayer#disconnect+PlayerList#remove.Dummieslädt das Backend perClass.forName- passt es nicht zur Server-Version (LinkageErroro.ä.), sind Dummies aus und nur das wird gemeldet. Nach jedem MC-Update hier zuerst prüfen. - Lebenszyklus:
Loader#dummy(indata.yml) ist der Wunsch des Owners;Dummies#sync(loader)sorgt dafür, dass der Dummy genau dann da ist, wenn der Loader aktiv, nicht gesperrt oder pausiert, seine Welt geladen unddummies.enabledan ist.LoaderManagerruftsyncbei an/aus, Löschen, Pause, Welt laden undapplyAll. Gespawnt wird einen Tick später (nicht mitten in fremden Events), ein Wächter alle 5 s holt verschwundene, getötete oder abgesetzte Dummies zurück. - Eigenschaften: Kreativmodus (unverwundbar, Mobs ignorieren ihn, keine Phantome), nie mit
doTickgetickt (bewegt sich nicht, isst nicht, sammelt nichts, keine Statistiken),setPersistent(false)(Paper speichert keine Spielerdaten),setSleepingIgnored(true), Sicht-/Simulationsdistanz = Loader-Radius, Sende-Distanz 2. Sitzt auf einem unsichtbarenItemDisplay(dummies.sit). UUID = Namens-UUID aus der Loader-ID, Name[D<nr>]<owner>(Klammern gibt es in echten Namen nicht, also nie eine Namensgleichheit). - Für den Rest unsichtbar: Join/Quit/Advancement/Tod ohne Meldung, Tab-Liste per
Player#unlistPlayer, Serverliste (PaperServerListPingEvent) und „Server voll“ (PlayerServerFullCheckEvent) ohne Dummies. Kick, Teleport, Absetzen, Spielmodus-Wechsel und Schaden durch andere werden abgebrochen. Markierung für andere Plugins: MetadataNPC(Citizens-Konvention) und PDC-Schlüsselmrchunks:dummy; MRsleep, MRadmin und MRpvp prüfen beides (jeweils KlasseNpcs). - Skin: Eigenschaft
texturesdes Owners - beim Join des Owners aktualisiert, sonst einmal perPlayerProfile#updategeholt, gespeichert inskins.yml.
11. Verknüpfte Accounts
- Daten:
LoaderManager.links(Zweit → Haupt, keine Ketten).main(uuid)liefert den Haupt-Account,group(uuid)alle Accounts. Loader gehören immer dem Haupt-Account:AdminActions.accountOf(player)ist überall maßgeblich, wo früher die UUID des Spielers stand (Owner-Prüfung, Setzen, Zählen, Einladungen, Menüs). Limits werden immer für den Haupt-Account berechnet,groupBanprüft die ganze Gruppe,notifyOwnererreicht alle Online-Accounts der Gruppe. - Ablauf:
LinkRequests#request(Haupt-Account fragt) → Dialog und Chat-Buttons beim anderen (läuft nachlinked-accounts.request-secondsab) →AdminActions#linkprüft alles erneut (linkProblem) →LoaderManager#linkübernimmt Loader (mit neuen Nummern) und Einladungen für alle Loader und entfernt Einladungen innerhalb der Gruppe. - Limit offline: Weil der Zweit-Account das Budget des (vielleicht offline) Haupt-Accounts nutzt, merkt sich
limit()Permission-Limits inknown-limitsund verwendet sie, solange der Account offline ist.
Quelle: mrchunks/docs/DOKUMENTATION.md ↗