Implement EndOpeningSchedule for managing End dimension access timing

This commit is contained in:
besiece committed 2026-11-08 18:55:11 +01:00
1 parent 3b43b594f4
commit 02d396db68
4 files changed
+150 -1

No files matched your search

@@ -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`.
@@ -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<Instant> previousEndOpenAt = Optional.empty();
private Optional<Instant> 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<Instant> endOpenAt = this.scheduler.getTime("end-open-at");
Optional<Instant> 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;
}
}
}
@@ -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();
}
@@ -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