Refactor DimensionScheduler to support sound settings in message groups and effects

This commit is contained in:
besiece committed 2026-11-08 20:44:55 +01:00
1 parent 23d8fc85de
commit e558ae3fac
4 files changed
+128 -22

No files matched your search

@@ -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<String> texts, Optional<SoundSettings> sound) {
}
public record Effect(String name, int minutesBeforeOpening, String message, Optional<SoundSettings> 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<String, Instant> times = Map.of();
private Map<String, List<String>> messages = Map.of();
private Map<String, MessageGroup> messages = Map.of();
private Map<String, List<Effect>> 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<String, List<String>> loadedMessages = readMessages(messageSection == null ? section(defaults, "messages") : messageSection);
Map<String, MessageGroup> 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);
@@ -90,12 +103,18 @@ public class DimensionScheduler extends Appliance {
throw new IllegalArgumentException(parent.getCurrentPath() + "." + key + " muss ein YAML-Abschnitt sein.");
}
private static Map<String, List<String>> readMessages(ConfigurationSection section) {
private static Map<String, MessageGroup> readMessages(ConfigurationSection section) {
if(section == null) return Map.of();
Map<String, List<String>> messages = new HashMap<>();
Map<String, MessageGroup> 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.");
Object configured = entry.getValue();
Optional<SoundSettings> 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<String> 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<SoundSettings> 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<Instant> 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<String> 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<Effect> getEffects(String name) {
@@ -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.
@@ -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<String> messages = this.scheduler.getMessages("end-activation-denied");
MessageGroup group = this.scheduler.getMessageGroup("end-activation-denied");
List<String> 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> sound = effect.sound().map(SoundSettings::asSound);
Bukkit.getOnlinePlayers().forEach(player -> send(player, message, sound));
}
private static void send(Player player, Component message, Optional<Sound> sound) {
player.sendMessage(message);
sound.ifPresent(value -> player.playSound(value, Sound.Emitter.self()));
}
}
@@ -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