Enhance DimensionScheduler and EndOpeningEffects with dynamic effect handling and YAML configuration support
This commit is contained in:
1 parent
f3f19a3370
commit
20616f1e02
6 files changed
+238
-86
No files matched your search
+98
-4
@@ -3,26 +3,40 @@ package eu.mhsl.craftattack.spawn.common.appliances.tooling.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 org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/** Reads named times only; applying them to dimension access is handled separately. */
|
||||
/** Reads named times, message groups and effect definitions; execution belongs to their consumers. */
|
||||
public class DimensionScheduler extends Appliance {
|
||||
public record Effect(String name, int minutesBeforeOpening, String message) {
|
||||
public Duration leadTime() {
|
||||
return Duration.ofMinutes(this.minutesBeforeOpening);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String FILE_NAME = "dimension-schedule.yml";
|
||||
private final File scheduleFile = new File(Main.instance().getDataFolder(), FILE_NAME);
|
||||
private Map<String, Instant> times = Map.of();
|
||||
private Map<String, List<String>> messages = Map.of();
|
||||
private Map<String, List<Effect>> effects = Map.of();
|
||||
private Instant loadedAt = Instant.now();
|
||||
|
||||
@Override
|
||||
@@ -31,23 +45,95 @@ public class DimensionScheduler extends Appliance {
|
||||
if(!this.scheduleFile.exists()) Main.instance().saveResource(FILE_NAME, false);
|
||||
this.reload();
|
||||
} catch(IOException | InvalidConfigurationException | IllegalArgumentException e) {
|
||||
Main.logger().log(Level.WARNING, "Could not load " + FILE_NAME + "; keeping previous times", e);
|
||||
Main.logger().log(Level.WARNING, "Could not load " + FILE_NAME + "; keeping previous configuration", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Validates all entries before replacing the current times. Never creates or writes the file. */
|
||||
/** Validates the complete configuration before replacing it. Never creates or writes the file. */
|
||||
public void reload() throws IOException, InvalidConfigurationException {
|
||||
YamlConfiguration configuration = new YamlConfiguration();
|
||||
configuration.load(this.scheduleFile);
|
||||
YamlConfiguration defaults = this.readDefaults();
|
||||
|
||||
Map<String, Instant> loadedTimes = new HashMap<>();
|
||||
for(Map.Entry<String, Object> entry : configuration.getValues(false).entrySet()) {
|
||||
ConfigurationSection timeSection = section(configuration, "times");
|
||||
boolean legacyTimes = timeSection == null;
|
||||
if(legacyTimes) timeSection = configuration;
|
||||
for(Map.Entry<String, Object> entry : timeSection.getValues(false).entrySet()) {
|
||||
if(legacyTimes && List.of("times", "messages", "effects").contains(entry.getKey())) continue;
|
||||
readTime(entry.getKey(), entry.getValue()).ifPresent(time -> loadedTimes.put(entry.getKey(), time));
|
||||
}
|
||||
ConfigurationSection messageSection = section(configuration, "messages");
|
||||
ConfigurationSection effectSection = section(configuration, "effects");
|
||||
Map<String, List<String>> loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection);
|
||||
Map<String, List<Effect>> loadedEffects = readEffects(effectSection == null ? section(defaults, "effects") : effectSection);
|
||||
|
||||
this.times = Map.copyOf(loadedTimes);
|
||||
this.messages = loadedMessages;
|
||||
this.effects = loadedEffects;
|
||||
this.loadedAt = Instant.now();
|
||||
}
|
||||
|
||||
private YamlConfiguration readDefaults() throws IOException, InvalidConfigurationException {
|
||||
YamlConfiguration defaults = new YamlConfiguration();
|
||||
try(InputStream resource = Main.instance().getResource(FILE_NAME)) {
|
||||
if(resource == null) throw new IOException("Missing bundled " + FILE_NAME);
|
||||
defaults.load(new InputStreamReader(resource, StandardCharsets.UTF_8));
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
private static ConfigurationSection section(ConfigurationSection parent, String key) {
|
||||
Object value = parent.get(key);
|
||||
if(value == null) return null;
|
||||
if(value instanceof ConfigurationSection section) return section;
|
||||
throw new IllegalArgumentException(parent.getCurrentPath() + "." + key + " muss ein YAML-Abschnitt sein.");
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> readMessages(ConfigurationSection section) {
|
||||
if(section == null) return Map.of();
|
||||
Map<String, List<String>> messages = new HashMap<>();
|
||||
for(Map.Entry<String, Object> entry : section.getValues(false).entrySet()) {
|
||||
if(!(entry.getValue() instanceof List<?> values)) {
|
||||
throw new IllegalArgumentException("messages." + entry.getKey() + " muss eine Liste von Texten sein.");
|
||||
}
|
||||
List<String> texts = new ArrayList<>();
|
||||
for(Object value : values) {
|
||||
if(!(value instanceof String text) || text.isBlank()) {
|
||||
throw new IllegalArgumentException("messages." + entry.getKey() + " enthält einen ungültigen Text.");
|
||||
}
|
||||
texts.add(text);
|
||||
}
|
||||
messages.put(entry.getKey(), List.copyOf(texts));
|
||||
}
|
||||
return Map.copyOf(messages);
|
||||
}
|
||||
|
||||
private static Map<String, List<Effect>> readEffects(ConfigurationSection section) {
|
||||
if(section == null) return Map.of();
|
||||
Map<String, List<Effect>> effects = new HashMap<>();
|
||||
for(String groupName : section.getKeys(false)) {
|
||||
ConfigurationSection group = section(section, groupName);
|
||||
if(group == null) throw new IllegalArgumentException("effects." + groupName + " muss ein YAML-Abschnitt sein.");
|
||||
List<Effect> definitions = new ArrayList<>();
|
||||
for(String name : group.getKeys(false)) {
|
||||
ConfigurationSection effect = section(group, name);
|
||||
if(effect == null) throw new IllegalArgumentException("effects." + groupName + "." + name + " muss ein YAML-Abschnitt sein.");
|
||||
Object minutes = effect.get("minutes-before-opening");
|
||||
Object message = effect.get("message");
|
||||
if(!(minutes instanceof Integer count) || count < 0) {
|
||||
throw new IllegalArgumentException(effect.getCurrentPath() + ".minutes-before-opening muss eine nicht negative ganze Zahl sein.");
|
||||
}
|
||||
if(!(message instanceof String text) || text.isBlank()) {
|
||||
throw new IllegalArgumentException(effect.getCurrentPath() + ".message muss ein nicht leerer Text sein.");
|
||||
}
|
||||
definitions.add(new Effect(name, count, text));
|
||||
}
|
||||
effects.put(groupName, List.copyOf(definitions));
|
||||
}
|
||||
return Map.copyOf(effects);
|
||||
}
|
||||
|
||||
private static Optional<Instant> readTime(String key, Object configured) {
|
||||
if(configured == null) return Optional.empty();
|
||||
if(!(configured instanceof String value)) {
|
||||
@@ -70,6 +156,14 @@ public class DimensionScheduler extends Appliance {
|
||||
return this.times;
|
||||
}
|
||||
|
||||
public List<String> getMessages(String name) {
|
||||
return this.messages.getOrDefault(name, List.of());
|
||||
}
|
||||
|
||||
public List<Effect> getEffects(String name) {
|
||||
return this.effects.getOrDefault(name, List.of());
|
||||
}
|
||||
|
||||
/** Wall-clock time of the last successful load, before consumers check the new times. */
|
||||
public Instant getLoadedAt() {
|
||||
return this.loadedAt;
|
||||
|
||||
+52
-21
@@ -4,19 +4,51 @@
|
||||
|
||||
| Klasse | Aufgabe und Erstellung |
|
||||
| --- | --- |
|
||||
| `DimensionScheduler` | Automatisch geladene Appliance. Erstellt die fehlende `plugins/spawn/dimension-schedule.yml` aus der Vorlage und liest beliebige benannte Zeitpunkte. Kennt keine End-Aktionen. |
|
||||
| `DimensionScheduler` | Automatisch geladene Appliance. Erstellt die fehlende `plugins/spawn/dimension-schedule.yml` aus der Vorlage und liest benannte Zeitpunkte, Textgruppen und Effektdefinitionen. Kennt keine End-Aktionen. |
|
||||
| `DimensionScheduleCommand` | Wird über die Appliance registriert. `/dimensionSchedule reload` liest die Datei für Konsole und OPs neu ein. Bei Fehlern bleiben alle bisherigen Werte erhalten. |
|
||||
| `EndPrevent` | Besitzt die `EndOpeningEffects`. Erstellt in `onEnable()` eine `EndOpeningSchedule`, wenn die Scheduler-Appliance vorhanden ist, und stoppt sie in `onDisable()`. Verwaltet den gemeinsamen Zustandswechsel. |
|
||||
| `EndOpeningSchedule` | Besitzt einen Bukkit-Task. Startet im nächsten Tick nach der Initialisierung aller Appliances und prüft dann alle 20 Ticks die Uhrzeit und Ankündigungsstufen. |
|
||||
| `EndOpeningEffects` | Enthält die fünf zufälligen Aktivierungstexte und die fünf Ankündigungs-/Öffnungstexte. `Stage` definiert die Vorlaufzeiten, `play(stage)` führt die Stufeneffekte aus. |
|
||||
| `EndOpeningEffects` | Verwendet die geladenen Text- und Effektgruppen. Wählt Aktivierungstexte zufällig aus und gibt Effekte über `play(effect)` aus. |
|
||||
|
||||
## Öffnungszeiten
|
||||
## YAML und dynamische Effekte
|
||||
|
||||
```yaml
|
||||
end-open-at: "2026-11-15T18:00:00+01:00"
|
||||
outer-end-open-at: "2026-11-16T18:00:00+01:00"
|
||||
times:
|
||||
end-open-at: "2026-11-15T18:00:00+01:00"
|
||||
outer-end-open-at: "2026-11-16T18:00:00+01:00"
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
|
||||
effects:
|
||||
end-opening:
|
||||
five-minutes-before:
|
||||
minutes-before-opening: 5
|
||||
message: "Etwas regt sich in der Leere ..."
|
||||
opened:
|
||||
minutes-before-opening: 0
|
||||
message: "Der Weg ins End ist nun offen!"
|
||||
```
|
||||
|
||||
`times` kann beliebige benannte Zeitpunkte enthalten. `messages.end-activation-denied`
|
||||
enthält beliebig viele Texte für die zufällige Actionbar-Meldung.
|
||||
Unter `effects.end-opening` können frei benannte Effekte ergänzt oder entfernt werden.
|
||||
`minutes-before-opening` ist eine nicht negative ganze Zahl: Ein positiver Wert gibt den
|
||||
Abstand zur End-Öffnung in Minuten an, `0` kennzeichnet einen Effekt beim tatsächlichen Öffnen.
|
||||
Texte und Vorlaufzeiten stehen ausschließlich in der YAML; eine feste Stufen-Enum gibt es nicht.
|
||||
Die mitgelieferte Vorlage enthält die bisherigen zehn Originaltexte mit 240, 60, 10, 1 und 0 Minuten.
|
||||
|
||||
Die ursprünglichen flachen Zeitangaben (`end-open-at` direkt im YAML-Hauptabschnitt) bleiben
|
||||
lesbar, solange kein `times`-Abschnitt vorhanden ist. Fehlt der gesamte `messages`- oder
|
||||
`effects`-Abschnitt, wird nur für diesen Abschnitt die mitgelieferte Vorlage im Arbeitsspeicher
|
||||
verwendet. Bestehende Serverdateien werden nicht überschrieben oder automatisch ergänzt.
|
||||
Zum Anpassen die gewünschten Abschnitte aus der Vorlage in die Serverdatei übernehmen.
|
||||
Explizite leere Abschnitte (`messages: {}`, `effects: {}`) verwenden keine Standardgruppen;
|
||||
einzelne Gruppen lassen sich durch `end-activation-denied: []` beziehungsweise `end-opening: {}` leeren.
|
||||
|
||||
## Öffnungszeiten
|
||||
|
||||
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
|
||||
@@ -30,7 +62,8 @@ Weitere Namen können in der YAML ergänzt werden; nur die End-Klasse interpreti
|
||||
`/endPrevent preventEnd`, `/endPrevent allowEnd` und `/endPrevent onlyInnerEnd` bleiben bestehen.
|
||||
Commands und laufende zeitgesteuerte Übergänge verwenden `EndPrevent.setEndState()`.
|
||||
Der gemeinsame Zustandswechsel aktualisiert Zustand und Weltgrenze, speichert Änderungen in
|
||||
`config.yml` und führt beim Wechsel von `CLOSED` zu `NO_OUTER` oder `OPEN` den Effekt `OPENED` aus.
|
||||
`config.yml` und führt beim Wechsel von `CLOSED` zu `NO_OUTER` oder `OPEN` alle Effekte mit
|
||||
`minutes-before-opening: 0` aus.
|
||||
Bereits offene Zustände erzeugen bei erneutem Öffnen keine weitere Meldung. Die spätere
|
||||
Freigabe des Outer Ends (`NO_OUTER` zu `OPEN`) wiederholt die End-Öffnungsmeldung ebenfalls nicht.
|
||||
|
||||
@@ -45,40 +78,38 @@ Ein Reload mit unveränderten End-Terminen setzt einen manuellen Zustand nicht z
|
||||
|
||||
## Ankündigungen und Portalversuche
|
||||
|
||||
| Stufe | Zeitpunkt und Ausgabe |
|
||||
| --- | --- |
|
||||
| `FOUR_HOURS_BEFORE` | 4 Stunden vor `end-open-at`: unheimliche Stille |
|
||||
| `ONE_HOUR_BEFORE` | 1 Stunde vorher: schwere Luft |
|
||||
| `TEN_MINUTES_BEFORE` | 10 Minuten vorher: Flügelschlagen |
|
||||
| `ONE_MINUTE_BEFORE` | 1 Minute vorher: Schatten |
|
||||
| `OPENED` | Bei einer tatsächlichen Öffnung: Riss in der Realität |
|
||||
|
||||
Vorankündigungen erscheinen einmalig für alle Online-Spieler im Chat, solange das End
|
||||
geschlossen ist. Bereits vergangene Stufen werden beim Start oder bei einem neuen End-Termin
|
||||
übersprungen. Unveränderte Reloads, neue Outer-End-Termine und Änderungen anderer YAML-Einträge
|
||||
setzen die bereits behandelten Stufen nicht zurück. Bei Verzögerungen wird höchstens die
|
||||
zuletzt fällige Vorankündigung ausgegeben. Ist die Öffnung bereits fällig, entfallen alte Teaser.
|
||||
zuletzt fällige Vorankündigungszeit berücksichtigt. Mehrere Effekte mit genau derselben
|
||||
Vorlaufzeit werden gemeinsam ausgeführt. Ist die Öffnung bereits fällig, entfallen alte Teaser.
|
||||
Während einer manuellen Freigabe fällige Teaser werden still als behandelt markiert.
|
||||
|
||||
Geänderte Definitionen unter `effects.end-opening` werden nach einem erfolgreichen Reload
|
||||
ebenfalls übernommen. Bereits vor diesem Reload vergangene Ankündigungen werden übersprungen;
|
||||
zukünftige Effekte laufen mit den neuen Texten und Minutenwerten. Reine Textänderungen in
|
||||
`messages` setzen die Ankündigungen nicht zurück. Ungültige Zeiten, Minutenwerte oder Texte
|
||||
lassen den gesamten Reload scheitern und behalten die bisherige Konfiguration.
|
||||
|
||||
Für einen geänderten Termin gilt der erfolgreiche Ladezeitpunkt als Grenze für verpasste
|
||||
Stufen. Wird eine Ankündigung erst zwischen dem Reload und der nächsten Sekundenprüfung
|
||||
fällig, wird sie regulär ausgeführt. Dasselbe gilt für eine in diesem Zeitraum fällige Öffnung.
|
||||
Beim erneuten Aktivieren von EndPrevent gilt frühestens dessen Startzeitpunkt als Grenze.
|
||||
|
||||
Ein Rechtsklick mit Enderauge auf einen leeren, gesperrten Endportalrahmen wird für beide Hände
|
||||
abgebrochen. Nur der auslösende Spieler erhält einen der fünf zufälligen Texte in der Actionbar,
|
||||
abgebrochen. Nur der auslösende Spieler erhält einen zufälligen konfigurierten Text in der Actionbar,
|
||||
höchstens einmal pro Server-Tick. Die Spieler-Zuordnung wird beim Verlassen entfernt.
|
||||
Die bestehenden Meldungen beim Betreten gesperrter Portale und Gateways bleiben getrennt davon.
|
||||
|
||||
## Erweiterungen und Leistung
|
||||
|
||||
Die Originaltexte stehen ausschließlich in `EndOpeningEffects`. Zusätzliche Sounds oder
|
||||
andere Effekte können in `play(stage)` ergänzt werden und werden bei manueller sowie
|
||||
zeitgesteuerter Öffnung gleichermaßen ausgeführt. Weitere Vorankündigungen erhalten einen
|
||||
neuen `Stage`-Eintrag mit Vorlaufzeit und Text; der Zeitplan berücksichtigt sie automatisch.
|
||||
Zusätzliche Sounds oder andere Effektarten können später zentral in `EndOpeningEffects.play(effect)`
|
||||
ergänzt werden und werden bei manueller sowie zeitgesteuerter Öffnung gleichermaßen ausgeführt.
|
||||
Neue Ankündigungen benötigen nur einen weiteren YAML-Eintrag mit Minutenwert und Text.
|
||||
|
||||
Es bleibt bei einem Task mit wenigen Zeitvergleichen pro Sekunde und einer kleinen
|
||||
`EnumSet`-Menge für behandelte Stufen. Die Prüfung arbeitet im Arbeitsspeicher. Chat,
|
||||
Menge behandelter Effektnamen. Die Prüfung arbeitet im Arbeitsspeicher. Chat,
|
||||
Zustandswechsel und spätere Bukkit-Effekte laufen auf dem Server-Hauptthread. Gespeichert
|
||||
wird nur bei tatsächlichen Zustandsänderungen in `config.yml`; die Termin-YAML wird nach
|
||||
ihrer erstmaligen Erstellung ausschließlich gelesen. Es gibt keine persistenten Ausführungsstatus.
|
||||
+23
-34
@@ -1,56 +1,45 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.Effect;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/** All End-opening presentation lives here; add sounds or other effects in play(). */
|
||||
/** Presents YAML-defined End effects. Add sounds or other effects in play(). */
|
||||
final class EndOpeningEffects {
|
||||
enum Stage {
|
||||
FOUR_HOURS_BEFORE(Duration.ofHours(4),
|
||||
"Eine unheimliche Stille scheint sich langsam über die Welt auszubreiten. Du hast das seltsame Gefühl, beobachtet zu werden ..."),
|
||||
ONE_HOUR_BEFORE(Duration.ofHours(1),
|
||||
"Die Luft fühlt sich plötzlich ungewöhnlich schwer an. Du hast das Gefühl, als würde eine unsichtbare Kraft auf dir lasten ..."),
|
||||
TEN_MINUTES_BEFORE(Duration.ofMinutes(10),
|
||||
"Ein seltsames Geräusch hallt durch die Welt. Könnte es das Schlagen gewaltiger Flügel sein ...?"),
|
||||
ONE_MINUTE_BEFORE(Duration.ofMinutes(1),
|
||||
"Ein gewaltiger Schatten scheint über die Welt zu ziehen. Du blickst zum Himmel, doch dort ist nichts zu sehen ..."),
|
||||
OPENED(Duration.ZERO,
|
||||
"Ein gewaltiger Riss öffnet sich in der Realität! Die Leere erwacht und die letzte Grenze fällt. Der Weg ins End ist nun offen!");
|
||||
private @Nullable DimensionScheduler scheduler;
|
||||
|
||||
private final Duration leadTime;
|
||||
private final String message;
|
||||
|
||||
Stage(Duration leadTime, String message) {
|
||||
this.leadTime = leadTime;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
Duration leadTime() {
|
||||
return this.leadTime;
|
||||
}
|
||||
void configure(DimensionScheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
private static final List<String> ACTIVATION_DENIED_MESSAGES = List.of(
|
||||
"Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ...",
|
||||
"Der Rahmen fühlt sich ungewöhnlich kalt an. Du hast das Gefühl, als würde er dir die Wärme aus den Fingern ziehen …",
|
||||
"Die Luft über dem leeren Rahmen scheint ungewöhnlich dunkel. Du hast das Gefühl, in einen bodenlosen Abgrund zu blicken ...",
|
||||
"Für einen kurzen Moment scheint die Umgebung vollkommen still zu werden. Du fragst dich, ob du dir das nur eingebildet hast ...",
|
||||
"Ein schwaches, violettes Licht flackert zwischen den Augen auf. Etwas scheint dich zu beobachten ..."
|
||||
);
|
||||
List<Effect> getConfiguredEffects() {
|
||||
return this.scheduler == null ? List.of() : this.scheduler.getEffects("end-opening");
|
||||
}
|
||||
|
||||
void denyActivation(Player player) {
|
||||
String message = ACTIVATION_DENIED_MESSAGES.get(ThreadLocalRandom.current().nextInt(ACTIVATION_DENIED_MESSAGES.size()));
|
||||
if(this.scheduler == null) return;
|
||||
List<String> messages = this.scheduler.getMessages("end-activation-denied");
|
||||
if(messages.isEmpty()) return;
|
||||
String message = messages.get(ThreadLocalRandom.current().nextInt(messages.size()));
|
||||
player.sendActionBar(Component.text(message, NamedTextColor.RED));
|
||||
}
|
||||
|
||||
void play(Stage stage) {
|
||||
Component message = Component.text(stage.message, stage == Stage.OPENED ? NamedTextColor.LIGHT_PURPLE : NamedTextColor.GRAY);
|
||||
void playOpening() {
|
||||
for(Effect effect : this.getConfiguredEffects()) {
|
||||
if(effect.minutesBeforeOpening() == 0) this.play(effect);
|
||||
}
|
||||
}
|
||||
|
||||
void play(Effect effect) {
|
||||
NamedTextColor color = effect.minutesBeforeOpening() == 0 ? NamedTextColor.LIGHT_PURPLE : NamedTextColor.GRAY;
|
||||
Component message = Component.text(effect.message(), color);
|
||||
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
}
|
||||
+29
-19
@@ -1,26 +1,29 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent.EndOpeningEffects.Stage;
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.Effect;
|
||||
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.EnumSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 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 static final EnumSet<Stage> ANNOUNCEMENT_STAGES = EnumSet.complementOf(EnumSet.of(Stage.OPENED));
|
||||
private final EndPrevent endPrevent;
|
||||
private final DimensionScheduler scheduler;
|
||||
private final EndOpeningEffects effects;
|
||||
private final EnumSet<Stage> handledAnnouncements = EnumSet.noneOf(Stage.class);
|
||||
private final Set<String> handledAnnouncements = new HashSet<>();
|
||||
private List<Effect> previousEffects = List.of();
|
||||
private @Nullable BukkitTask task;
|
||||
private Optional<Instant> previousEndOpenAt = Optional.empty();
|
||||
private Optional<Instant> previousOuterEndOpenAt = Optional.empty();
|
||||
@@ -50,15 +53,19 @@ final class EndOpeningSchedule {
|
||||
private void checkOpeningTimes() {
|
||||
Optional<Instant> endOpenAt = this.scheduler.getTime("end-open-at");
|
||||
Optional<Instant> outerEndOpenAt = this.scheduler.getTime("outer-end-open-at");
|
||||
List<Effect> configuredEffects = this.effects.getConfiguredEffects();
|
||||
Instant now = Instant.now();
|
||||
boolean endTimeChanged = !endOpenAt.equals(this.previousEndOpenAt);
|
||||
boolean restoreSchedule = !this.initialized || endTimeChanged
|
||||
|| !outerEndOpenAt.equals(this.previousOuterEndOpenAt);
|
||||
Instant restoreAt = this.scheduler.getLoadedAt();
|
||||
if(!this.initialized && this.startedAt.isAfter(restoreAt)) restoreAt = this.startedAt;
|
||||
if(!this.initialized || endTimeChanged) this.resetAnnouncements(endOpenAt, restoreAt);
|
||||
if(!this.initialized || endTimeChanged || !configuredEffects.equals(this.previousEffects)) {
|
||||
this.resetAnnouncements(endOpenAt, configuredEffects, restoreAt);
|
||||
}
|
||||
this.previousEndOpenAt = endOpenAt;
|
||||
this.previousOuterEndOpenAt = outerEndOpenAt;
|
||||
this.previousEffects = configuredEffects;
|
||||
this.initialized = true;
|
||||
|
||||
if(endOpenAt.isEmpty() && outerEndOpenAt.isEmpty()) {
|
||||
@@ -89,7 +96,7 @@ final class EndOpeningSchedule {
|
||||
}
|
||||
this.previousScheduledState = scheduledState;
|
||||
}
|
||||
this.announceDueStages(endOpenAt, now);
|
||||
this.announceDueEffects(endOpenAt, configuredEffects, now);
|
||||
}
|
||||
|
||||
private static EndPrevent.State stateAt(Instant at, Optional<Instant> endOpenAt, Optional<Instant> outerEndOpenAt) {
|
||||
@@ -99,35 +106,38 @@ final class EndOpeningSchedule {
|
||||
}
|
||||
|
||||
/** A new opening time starts a new cycle; announcements already in the past are consumed. */
|
||||
private void resetAnnouncements(Optional<Instant> endOpenAt, Instant now) {
|
||||
private void resetAnnouncements(Optional<Instant> endOpenAt, List<Effect> configuredEffects, Instant cutoff) {
|
||||
this.handledAnnouncements.clear();
|
||||
if(endOpenAt.isEmpty()) return;
|
||||
for(Stage stage : ANNOUNCEMENT_STAGES) {
|
||||
if(endOpenAt.get().minus(stage.leadTime()).isBefore(now)) this.handledAnnouncements.add(stage);
|
||||
for(Effect effect : configuredEffects) {
|
||||
if(effect.minutesBeforeOpening() > 0 && endOpenAt.get().minus(effect.leadTime()).isBefore(cutoff)) {
|
||||
this.handledAnnouncements.add(effect.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void announceDueStages(Optional<Instant> endOpenAt, Instant now) {
|
||||
private void announceDueEffects(Optional<Instant> endOpenAt, List<Effect> configuredEffects, Instant now) {
|
||||
if(endOpenAt.isEmpty()) return;
|
||||
if(!now.isBefore(endOpenAt.get())) {
|
||||
// The opening transition owns OPENED. Never replay teasers after opening is due.
|
||||
this.handledAnnouncements.addAll(ANNOUNCEMENT_STAGES);
|
||||
// Zero-minute effects belong to the opening transition, never to this polling loop.
|
||||
for(Effect effect : configuredEffects) this.handledAnnouncements.add(effect.name());
|
||||
return;
|
||||
}
|
||||
|
||||
Stage latestDue = null;
|
||||
List<Effect> latestDue = new ArrayList<>();
|
||||
Instant latestDueAt = null;
|
||||
for(Stage stage : ANNOUNCEMENT_STAGES) {
|
||||
if(this.handledAnnouncements.contains(stage)) continue;
|
||||
Instant dueAt = endOpenAt.get().minus(stage.leadTime());
|
||||
for(Effect effect : configuredEffects) {
|
||||
if(effect.minutesBeforeOpening() == 0 || this.handledAnnouncements.contains(effect.name())) continue;
|
||||
Instant dueAt = endOpenAt.get().minus(effect.leadTime());
|
||||
if(now.isBefore(dueAt)) continue;
|
||||
this.handledAnnouncements.add(stage);
|
||||
this.handledAnnouncements.add(effect.name());
|
||||
if(latestDueAt == null || dueAt.isAfter(latestDueAt)) {
|
||||
latestDue = stage;
|
||||
latestDue.clear();
|
||||
latestDueAt = dueAt;
|
||||
}
|
||||
if(dueAt.equals(latestDueAt)) latestDue.add(effect);
|
||||
}
|
||||
// Consume all crossed milestones, but announce only the latest while the End is closed.
|
||||
if(latestDue != null && this.endPrevent.isEndClosed()) this.effects.play(latestDue);
|
||||
if(this.endPrevent.isEndClosed()) latestDue.forEach(this.effects::play);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -41,6 +41,7 @@ public class EndPrevent extends Appliance {
|
||||
.findFirst().orElse(null);
|
||||
if(scheduler == null) return;
|
||||
|
||||
this.openingEffects.configure(scheduler);
|
||||
this.openingSchedule = new EndOpeningSchedule(this, scheduler, this.openingEffects);
|
||||
this.openingSchedule.start();
|
||||
}
|
||||
@@ -73,7 +74,7 @@ public class EndPrevent extends Appliance {
|
||||
this.localConfig().set(this.endPreventKey, state.name());
|
||||
Configuration.saveChanges();
|
||||
if(announceOpening && wasClosed && !this.isEndClosed()) {
|
||||
this.openingEffects.play(EndOpeningEffects.Stage.OPENED);
|
||||
this.openingEffects.playOpening();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
# 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.
|
||||
# EndPrevent: Bis zu diesem Zeitpunkt bleibt das End geschlossen.
|
||||
end-open-at: "2026-11-08T19:16:34+01:00"
|
||||
# Optional: Nach der End-Öffnung nur die Hauptinsel bis zu diesem Zeitpunkt freigeben.
|
||||
outer-end-open-at: null
|
||||
# Datum mit UTC-Offset in Anführungszeichen; null bedeutet: kein Termin.
|
||||
# Weitere frei benannte Zeitpunkte können hier ergänzt werden.
|
||||
times:
|
||||
end-open-at: "2026-11-08T19:16:34+01:00"
|
||||
outer-end-open-at: null
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
- "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..."
|
||||
- "Der Rahmen fühlt sich ungewöhnlich kalt an. Du hast das Gefühl, als würde er dir die Wärme aus den Fingern ziehen …"
|
||||
- "Die Luft über dem leeren Rahmen scheint ungewöhnlich dunkel. Du hast das Gefühl, in einen bodenlosen Abgrund zu blicken ..."
|
||||
- "Für einen kurzen Moment scheint die Umgebung vollkommen still zu werden. Du fragst dich, ob du dir das nur eingebildet hast ..."
|
||||
- "Ein schwaches, violettes Licht flackert zwischen den Augen auf. Etwas scheint dich zu beobachten ..."
|
||||
|
||||
effects:
|
||||
end-opening:
|
||||
# Namen und Anzahl der Effekte sind frei wählbar.
|
||||
# Positive Minutenwerte: Vorankündigung im Chat, solange das End geschlossen ist.
|
||||
four-hours-before:
|
||||
minutes-before-opening: 240
|
||||
message: "Eine unheimliche Stille scheint sich langsam über die Welt auszubreiten. Du hast das seltsame Gefühl, beobachtet zu werden ..."
|
||||
one-hour-before:
|
||||
minutes-before-opening: 60
|
||||
message: "Die Luft fühlt sich plötzlich ungewöhnlich schwer an. Du hast das Gefühl, als würde eine unsichtbare Kraft auf dir lasten ..."
|
||||
ten-minutes-before:
|
||||
minutes-before-opening: 10
|
||||
message: "Ein seltsames Geräusch hallt durch die Welt. Könnte es das Schlagen gewaltiger Flügel sein ...?"
|
||||
one-minute-before:
|
||||
minutes-before-opening: 1
|
||||
message: "Ein gewaltiger Schatten scheint über die Welt zu ziehen. Du blickst zum Himmel, doch dort ist nichts zu sehen ..."
|
||||
# 0: Nur beim tatsächlichen Öffnen, gleichermaßen durch Command oder Zeitplan.
|
||||
opened:
|
||||
minutes-before-opening: 0
|
||||
message: "Ein gewaltiger Riss öffnet sich in der Realität! Die Leere erwacht und die letzte Grenze fällt. Der Weg ins End ist nun offen!"
|
||||
Reference in new issue
Block a user