From e558ae3facc80998cb6251d5d64949dcb69d6740 Mon Sep 17 00:00:00 2001 From: besiece Date: Sun, 8 Nov 2026 20:44:47 +0100 Subject: [PATCH] Refactor DimensionScheduler to support sound settings in message groups and effects --- .../DimensionScheduler.java | 75 ++++++++++++++++--- .../tooling/dimensionScheduler/README.md | 26 ++++++- .../tooling/endPrevent/EndOpeningEffects.java | 19 ++++- .../src/main/resources/dimension-schedule.yml | 30 ++++++-- 4 files changed, 128 insertions(+), 22 deletions(-) diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/DimensionScheduler.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/DimensionScheduler.java index f3d7a60..7497f39 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/DimensionScheduler.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/dimensionScheduler/DimensionScheduler.java @@ -3,6 +3,9 @@ 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 net.kyori.adventure.key.InvalidKeyException; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; @@ -26,16 +29,26 @@ import java.util.logging.Level; /** 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 record SoundSettings(String name, float volume, float pitch) { + public Sound asSound() { + return Sound.sound(Key.key(this.name), Sound.Source.MASTER, this.volume, this.pitch); + } + } + + public record MessageGroup(List texts, Optional sound) { + } + + public record Effect(String name, int minutesBeforeOpening, String message, Optional sound) { public Duration leadTime() { return Duration.ofMinutes(this.minutesBeforeOpening); } } private static final String FILE_NAME = "dimension-schedule.yml"; + private static final MessageGroup EMPTY_MESSAGE_GROUP = new MessageGroup(List.of(), Optional.empty()); private final File scheduleFile = new File(Main.instance().getDataFolder(), FILE_NAME); private Map times = Map.of(); - private Map> messages = Map.of(); + private Map messages = Map.of(); private Map> effects = Map.of(); private Instant loadedAt = Instant.now(); @@ -65,7 +78,7 @@ public class DimensionScheduler extends Appliance { } ConfigurationSection messageSection = section(configuration, "messages"); ConfigurationSection effectSection = section(configuration, "effects"); - Map> loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection); + Map loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection); Map> loadedEffects = readEffects(effectSection == null ? section(defaults, "effects") : effectSection); this.times = Map.copyOf(loadedTimes); @@ -90,12 +103,18 @@ public class DimensionScheduler extends Appliance { throw new IllegalArgumentException(parent.getCurrentPath() + "." + key + " muss ein YAML-Abschnitt sein."); } - private static Map> readMessages(ConfigurationSection section) { + private static Map readMessages(ConfigurationSection section) { if(section == null) return Map.of(); - Map> messages = new HashMap<>(); + Map messages = new HashMap<>(); for(Map.Entry entry : section.getValues(false).entrySet()) { - if(!(entry.getValue() instanceof List values)) { - throw new IllegalArgumentException("messages." + entry.getKey() + " muss eine Liste von Texten sein."); + Object configured = entry.getValue(); + Optional sound = Optional.empty(); + if(configured instanceof ConfigurationSection group) { + configured = group.get("texts"); + sound = readSound(group); + } + if(!(configured instanceof List values)) { + throw new IllegalArgumentException("messages." + entry.getKey() + " muss eine Textliste oder ein Abschnitt mit texts sein."); } List texts = new ArrayList<>(); for(Object value : values) { @@ -104,7 +123,7 @@ public class DimensionScheduler extends Appliance { } texts.add(text); } - messages.put(entry.getKey(), List.copyOf(texts)); + messages.put(entry.getKey(), new MessageGroup(List.copyOf(texts), sound)); } return Map.copyOf(messages); } @@ -127,13 +146,45 @@ public class DimensionScheduler extends Appliance { 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)); + definitions.add(new Effect(name, count, text, readSound(effect))); } effects.put(groupName, List.copyOf(definitions)); } return Map.copyOf(effects); } + private static Optional readSound(ConfigurationSection section) { + Object configured = section.get("sound"); + if(configured == null) return Optional.empty(); + if(!(configured instanceof String name)) { + throw new IllegalArgumentException(section.getCurrentPath() + ".sound muss ein Sound-Name sein."); + } + if(name.isBlank()) return Optional.empty(); + String key; + try { + key = Key.key(name.trim()).asString(); + } catch(InvalidKeyException e) { + throw new IllegalArgumentException("Ungültiger Sound-Name bei " + section.getCurrentPath() + ".sound: " + name, e); + } + float volume = readSoundNumber(section, "volume", true); + float pitch = readSoundNumber(section, "pitch", false); + return Optional.of(new SoundSettings(key, volume, pitch)); + } + + private static float readSoundNumber(ConfigurationSection section, String key, boolean allowZero) { + Object configured = section.get(key); + if(configured == null) return 1.0f; + if(!(configured instanceof Number number)) { + throw new IllegalArgumentException(section.getCurrentPath() + "." + key + " muss eine Zahl sein."); + } + float value = number.floatValue(); + if(!Float.isFinite(value) || (allowZero ? value < 0 : value <= 0)) { + throw new IllegalArgumentException(section.getCurrentPath() + "." + key + + (allowZero ? " muss endlich und mindestens 0 sein." : " muss endlich und größer als 0 sein.")); + } + return value; + } + private static Optional readTime(String key, Object configured) { if(configured == null) return Optional.empty(); if(!(configured instanceof String value)) { @@ -157,7 +208,11 @@ public class DimensionScheduler extends Appliance { } public List getMessages(String name) { - return this.messages.getOrDefault(name, List.of()); + return this.getMessageGroup(name).texts(); + } + + public MessageGroup getMessageGroup(String name) { + return this.messages.getOrDefault(name, EMPTY_MESSAGE_GROUP); } public List getEffects(String name) { 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 index b19d03f..bd60a3e 100644 --- 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 @@ -22,20 +22,40 @@ times: messages: end-activation-denied: - - "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..." + texts: + - "Die Augen scheinen dich für einen kurzen Moment anzustarren. Dir läuft ein kalter Schauer über den Rücken ..." + sound: "entity.warden.roar" + pitch: 0.2 + volume: 1.0 effects: end-opening: five-minutes-before: minutes-before-opening: 5 message: "Etwas regt sich in der Leere ..." + sound: "ambient.cave" + pitch: 0.5 + volume: 1.0 opened: minutes-before-opening: 0 message: "Der Weg ins End ist nun offen!" + sound: "block.end_portal.spawn" + pitch: 1.0 + volume: 1.0 ``` -`times` kann beliebige benannte Zeitpunkte enthalten. `messages.end-activation-denied` +`times` kann beliebige benannte Zeitpunkte enthalten. `messages.end-activation-denied.texts` enthält beliebig viele Texte für die zufällige private Chat-Nachricht. +Die bisherige direkte Textliste wird ebenfalls weiterhin gelesen. +`sound`, `pitch` und `volume` stehen direkt bei den zugehörigen Texten beziehungsweise Effekten. +Diese Sound-Angaben werden beim Reload mit eingelesen und direkt mit der jeweiligen Nachricht +einmal beim Empfänger abgespielt. Der Ablehnungs-Sound ist nur für den auslösenden Spieler hörbar; +Ankündigungs- und Öffnungssounds erhalten alle Empfänger der Chat-Nachricht jeweils an ihrer +eigenen Position. Es gibt keine zusätzlichen Tasks oder Schleifen für die Sound-Wiedergabe. +Ohne `sound` oder mit leerem Sound-Namen bleibt es bei der Textausgabe. `volume` und `pitch` +haben den Standardwert `1.0`; Lautstärke muss mindestens `0`, Pitch größer als `0` sein. +Ungültige Sound-Schlüssel oder nicht endliche Zahlen lassen den Reload wie andere ungültige +Konfigurationswerte scheitern und behalten die bisherige Konfiguration. 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. @@ -124,7 +144,7 @@ Alle Flavourtexte und zeitgesteuerten Chat-Effekte verwenden einheitlich End-Gr ## Erweiterungen und Leistung -Zusätzliche Sounds oder andere Effektarten können später zentral in `EndOpeningEffects.play(effect)` +Weitere 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. diff --git a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningEffects.java b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningEffects.java index 6c8b740..5501796 100644 --- a/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningEffects.java +++ b/common/src/main/java/eu/mhsl/craftattack/spawn/common/appliances/tooling/endPrevent/EndOpeningEffects.java @@ -2,16 +2,20 @@ 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 eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.MessageGroup; +import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduler.SoundSettings; import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduleText; +import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; import java.util.List; +import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; -/** Presents YAML-defined End effects. Add sounds or other effects in play(). */ +/** Sends each YAML-defined message together with its optional sound. */ final class EndOpeningEffects { private @Nullable DimensionScheduler scheduler; @@ -25,10 +29,11 @@ final class EndOpeningEffects { void denyActivation(Player player) { if(this.scheduler == null) return; - List messages = this.scheduler.getMessages("end-activation-denied"); + MessageGroup group = this.scheduler.getMessageGroup("end-activation-denied"); + List messages = group.texts(); if(messages.isEmpty()) return; String message = messages.get(ThreadLocalRandom.current().nextInt(messages.size())); - player.sendMessage(DimensionScheduleText.message(message)); + send(player, DimensionScheduleText.message(message), group.sound().map(SoundSettings::asSound)); } void playOpening() { @@ -39,6 +44,12 @@ final class EndOpeningEffects { void play(Effect effect) { Component message = DimensionScheduleText.message(effect.message()); - Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(message)); + Optional sound = effect.sound().map(SoundSettings::asSound); + Bukkit.getOnlinePlayers().forEach(player -> send(player, message, sound)); + } + + private static void send(Player player, Component message, Optional sound) { + player.sendMessage(message); + sound.ifPresent(value -> player.playSound(value, Sound.Emitter.self())); } } diff --git a/common/src/main/resources/dimension-schedule.yml b/common/src/main/resources/dimension-schedule.yml index 3ea15bc..db723c3 100644 --- a/common/src/main/resources/dimension-schedule.yml +++ b/common/src/main/resources/dimension-schedule.yml @@ -7,11 +7,16 @@ times: 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 ..." + texts: + - "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 ..." + # Ein Sound pro Nachricht, gemeinsam mit dem Nachrichten-Cooldown. + sound: "entity.warden.roar" + pitch: 0.2 + volume: 1.0 effects: end-opening: @@ -20,16 +25,31 @@ effects: 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 ..." + sound: "ambient.cave" + pitch: 0.5 + volume: 1.0 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 ..." + sound: "ambient.cave" + pitch: 0.5 + volume: 1.0 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 ...?" + sound: "ambient.cave" + pitch: 0.5 + volume: 1.0 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 ..." + sound: "entity.ender_dragon.growl" + pitch: 0.5 + volume: 1.0 # 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!" + sound: "block.end_portal.spawn" + pitch: 1.0 + volume: 1.0