diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/README.md b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/README.md new file mode 100644 index 0000000..31fcddc --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/README.md @@ -0,0 +1,44 @@ +# Dimension schedule + +## Zuständigkeiten und Lebenszyklus + +- `DimensionScheduler` wird als Appliance automatisch geladen. `onEnable()` legt + `plugins/spawn/dimension-schedule.yml` nur dann aus der mitgelieferten Vorlage an, + wenn die Datei fehlt, und liest alle benannten Zeitpunkte ein. Danach wird sie nur gelesen. +- `DimensionScheduleCommand` stellt `/dimensionSchedule reload` für Konsole und OPs bereit. + Ein fehlerhafter Reload behält alle bisherigen Zeitpunkte. +- `EndPrevent.onEnable()` erstellt eine `EndOpeningSchedule`, sofern die + `DimensionScheduler`-Appliance vorhanden ist. `EndPrevent.onDisable()` stoppt sie. +- `EndOpeningSchedule` übernimmt die End-Freigabe. Ihr einziger Bukkit-Task startet + im nächsten Tick, nachdem alle Appliances initialisiert wurden, und prüft danach alle + 20 Ticks (bei normaler Tickrate einmal pro Sekunde) die echte Uhrzeit. Die Prüfung liest + nur die geladenen Zeitpunkte im Arbeitsspeicher und läuft auf dem Server-Hauptthread. + +## Öffnungszeiten + +```yaml +end-open-at: "2026-11-15T18:00:00+01:00" +outer-end-open-at: "2026-11-16T18:00:00+01:00" +``` + +Vor `end-open-at` ist das End `CLOSED`, danach bis `outer-end-open-at` ist nur die +Hauptinsel verfügbar (`NO_OUTER`), anschließend ist es vollständig `OPEN`. +Ohne Outer-End-Termin öffnet `end-open-at` das gesamte End. Mit ausschließlich einem +Outer-End-Termin ist die Hauptinsel sofort verfügbar und das Outer End öffnet zum Termin. +Ein zukünftiger End-Termin hat Vorrang vor einem bereits vergangenen Outer-End-Termin. +Sind beide Einträge leer oder `null`, erfolgt keine automatische Zustandsänderung. +Weitere Namen können in der YAML ergänzt werden; nur die End-Klasse interpretiert diese zwei Namen. + +Beim Start wird der zur aktuellen Uhrzeit passende Zustand angewandt, auch bei vergangenen +Terminen. Geänderte Öffnungszeiten werden nach einem erfolgreichen Reload spätestens bei +der nächsten Prüfung berücksichtigt. Entfernte Termine heben den zuletzt gesetzten Zustand +nicht auf; die Steuerung bleibt dann vollständig manuell. + +## Manuelle Commands + +`/endPrevent preventEnd`, `/endPrevent allowEnd` und `/endPrevent onlyInnerEnd` bleiben bestehen. +Sie werden nicht bei jeder Prüfung überschrieben: Erst beim nächsten zeitgesteuerten +Zustandswechsel oder bei geänderten End-Terminen greift der Zeitplan erneut ein. +Ein Reload mit unveränderten End-Terminen setzt einen manuellen Zustand nicht zurück. +Automatische Änderungen gelten nur zur Laufzeit und schreiben keine Datei; manuelle +Commands speichern den Zustand weiterhin wie bisher in `config.yml`. diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningSchedule.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningSchedule.java new file mode 100644 index 0000000..2706589 --- /dev/null +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningSchedule.java @@ -0,0 +1,76 @@ +package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent; + +import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler; +import eu.mhsl.craftattack.spawn.core.Main; +import org.bukkit.Bukkit; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.util.Optional; + +/** + * End-specific interpretation of the named times. Created and stopped by EndPrevent. + * DimensionScheduler only supplies configuration; this class owns the single polling task. + */ +final class EndOpeningSchedule { + private final EndPrevent endPrevent; + private final DimensionScheduler scheduler; + private @Nullable BukkitTask task; + private Optional previousEndOpenAt = Optional.empty(); + private Optional previousOuterEndOpenAt = Optional.empty(); + private @Nullable EndPrevent.State previousScheduledState; + + EndOpeningSchedule(EndPrevent endPrevent, DimensionScheduler scheduler) { + this.endPrevent = endPrevent; + this.scheduler = scheduler; + } + + void start() { + this.stop(); + // Wait until the next tick so all appliances have finished loading, regardless of order. + this.task = Bukkit.getScheduler().runTaskTimer(Main.instance(), this::checkOpeningTimes, 1L, 20L); + } + + void stop() { + if(this.task == null) return; + this.task.cancel(); + this.task = null; + } + + private void checkOpeningTimes() { + Optional endOpenAt = this.scheduler.getTime("end-open-at"); + Optional outerEndOpenAt = this.scheduler.getTime("outer-end-open-at"); + boolean timesChanged = !endOpenAt.equals(this.previousEndOpenAt) + || !outerEndOpenAt.equals(this.previousOuterEndOpenAt); + this.previousEndOpenAt = endOpenAt; + this.previousOuterEndOpenAt = outerEndOpenAt; + + if(endOpenAt.isEmpty() && outerEndOpenAt.isEmpty()) { + this.previousScheduledState = null; + return; + } + + Instant now = Instant.now(); + EndPrevent.State scheduledState; + if(endOpenAt.isPresent() && now.isBefore(endOpenAt.get())) { + scheduledState = EndPrevent.State.CLOSED; + } else if(outerEndOpenAt.isPresent() && now.isBefore(outerEndOpenAt.get())) { + scheduledState = EndPrevent.State.NO_OUTER; + } else { + scheduledState = EndPrevent.State.OPEN; + } + + // Remember the schedule's state, not the current manual state: commands remain effective + // until the next scheduled transition or a change to the relevant opening times. + if(timesChanged || scheduledState != this.previousScheduledState) { + if(this.endPrevent.getEndPreventState() != scheduledState) { + this.endPrevent.applyEndState(scheduledState); + Main.logger().info("End opening schedule applied state " + scheduledState + + " (end-open-at=" + endOpenAt.orElse(null) + + ", outer-end-open-at=" + outerEndOpenAt.orElse(null) + ")"); + } + this.previousScheduledState = scheduledState; + } + } +} diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndPrevent.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndPrevent.java index cda488b..495322a 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndPrevent.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndPrevent.java @@ -1,5 +1,7 @@ package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent; +import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler; +import eu.mhsl.craftattack.spawn.core.Main; import eu.mhsl.craftattack.spawn.core.appliance.Appliance; import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand; import eu.mhsl.craftattack.spawn.core.config.Configuration; @@ -7,12 +9,14 @@ import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.List; public class EndPrevent extends Appliance { private final String endPreventKey = "endPrevent"; private State endPreventState; + private @Nullable EndOpeningSchedule openingSchedule; private final World endWorld = Bukkit.getWorlds().stream().filter(world -> world.getEnvironment().equals(World.Environment.THE_END)).findFirst().orElseThrow(); public enum State { @@ -27,6 +31,24 @@ public class EndPrevent extends Appliance { this.updateEndBorder(); } + @Override + public void onEnable() { + if(this.openingSchedule != null) this.openingSchedule.stop(); + DimensionScheduler scheduler = Main.instance().getAppliances().stream() + .filter(DimensionScheduler.class::isInstance) + .map(DimensionScheduler.class::cast) + .findFirst().orElse(null); + if(scheduler == null) return; + + this.openingSchedule = new EndOpeningSchedule(this, scheduler); + this.openingSchedule.start(); + } + + @Override + public void onDisable() { + if(this.openingSchedule != null) this.openingSchedule.stop(); + } + private void updateEndBorder() { if(this.endPreventState == State.NO_OUTER) this.endWorld.getWorldBorder().setSize(500); if(this.endPreventState == State.OPEN) this.endWorld.getWorldBorder().setSize(this.endWorld.getWorldBorder().getMaxSize()); @@ -35,6 +57,11 @@ public class EndPrevent extends Appliance { public void setEndState(State state) { this.localConfig().set(this.endPreventKey, state.name()); Configuration.saveChanges(); + this.applyEndState(state); + } + + /** Applies a runtime state; only manual commands persist it through setEndState(). */ + void applyEndState(State state) { this.endPreventState = state; this.updateEndBorder(); } diff --git a/common/src/main/resources/dimension-schedule.yml b/common/src/main/resources/dimension-schedule.yml index 5595eb3..6c74b79 100644 --- a/common/src/main/resources/dimension-schedule.yml +++ b/common/src/main/resources/dimension-schedule.yml @@ -1,5 +1,7 @@ # Datum mit UTC-Offset, z. B. "2026-11-15T18:00:00+01:00" # null bedeutet: kein Termin # Weitere Einträge mit frei wählbaren Namen können ergänzt werden. -end-open-at: null +# EndPrevent: Bis zu diesem Zeitpunkt bleibt das End geschlossen. +end-open-at: "2026-11-08T18:55:34+01:00" +# Optional: Nach der End-Öffnung nur die Hauptinsel bis zu diesem Zeitpunkt freigeben. outer-end-open-at: null