Add EndCommand and DimensionScheduleText for improved End dimension management and messaging
This commit is contained in:
1 parent
63dbefdde8
commit
23d8fc85de
9 files changed
+161
-32
No files matched your search
+7
-3
@@ -1,14 +1,13 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
class DimensionScheduleCommand extends ApplianceCommand<DimensionScheduler> {
|
||||
@@ -28,7 +27,12 @@ class DimensionScheduleCommand extends ApplianceCommand<DimensionScheduler> {
|
||||
} catch(IOException | InvalidConfigurationException | IllegalArgumentException e) {
|
||||
throw new Error("dimension-schedule.yml konnte nicht neu eingelesen werden: " + e.getMessage());
|
||||
}
|
||||
sender.sendMessage(Component.text("dimension-schedule.yml wurde neu eingelesen.", NamedTextColor.GREEN));
|
||||
Instant now = Instant.now();
|
||||
sender.sendMessage(DimensionScheduleText.message("Die Öffnungszeiten, Texte und Effekte wurden neu eingelesen."));
|
||||
sender.sendMessage(DimensionScheduleText.message("End-Termin: "
|
||||
+ DimensionScheduleText.appointment(this.getAppliance().getTime("end-open-at"), now)));
|
||||
sender.sendMessage(DimensionScheduleText.message("Nether-Termin: "
|
||||
+ DimensionScheduleText.appointment(this.getAppliance().getTime("nether-open-at"), now)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Shared appearance and German time formatting for dimension messages and commands. */
|
||||
public final class DimensionScheduleText {
|
||||
private static final TextColor END_GREEN = TextColor.color(0x80A71A);
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter
|
||||
.ofPattern("dd.MM.yyyy 'um' HH:mm:ss 'Uhr' z", Locale.GERMAN)
|
||||
.withZone(ZoneId.of("Europe/Berlin"));
|
||||
|
||||
private DimensionScheduleText() {
|
||||
}
|
||||
|
||||
public static Component message(String text) {
|
||||
return Component.text(text, END_GREEN);
|
||||
}
|
||||
|
||||
/** Round up partial minutes so a pending opening never reads as zero minutes away. */
|
||||
public static String remaining(Instant openingAt, Instant now) {
|
||||
Duration duration = Duration.between(now, openingAt);
|
||||
if(duration.isNegative() || duration.isZero()) return "0 Tagen, 0 Stunden und 0 Minuten";
|
||||
long minutes = duration.toMinutes();
|
||||
if(!duration.minusMinutes(minutes).isZero()) minutes++;
|
||||
long days = minutes / (24 * 60);
|
||||
long hours = minutes / 60 % 24;
|
||||
long minutePart = minutes % 60;
|
||||
return days + (days == 1 ? " Tag, " : " Tagen, ")
|
||||
+ hours + (hours == 1 ? " Stunde und " : " Stunden und ")
|
||||
+ minutePart + (minutePart == 1 ? " Minute" : " Minuten");
|
||||
}
|
||||
|
||||
public static String appointment(Optional<Instant> openingAt, Instant now) {
|
||||
if(openingAt.isEmpty()) return "nicht festgelegt";
|
||||
Instant time = openingAt.get();
|
||||
return DATE.format(time) + (time.isAfter(now)
|
||||
? " (in " + remaining(time, now) + ")"
|
||||
: " (Termin bereits erreicht)");
|
||||
}
|
||||
}
|
||||
+23
-4
@@ -5,10 +5,12 @@
|
||||
| Klasse | Aufgabe und Erstellung |
|
||||
| --- | --- |
|
||||
| `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. |
|
||||
| `DimensionScheduleCommand` | Wird über die Appliance registriert. `/dimensionSchedule reload` liest die Datei für Konsole und OPs neu ein und zeigt nur dem Ausführenden den End- und Nether-Termin. Bei Fehlern bleiben alle bisherigen Werte erhalten. |
|
||||
| `DimensionScheduleText` | Einheitliches End-Grün sowie deutsche Formatierung von Restzeiten und Terminen. Absolute Zeiten werden in `Europe/Berlin` mit Zeitzone ausgegeben. |
|
||||
| `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` | Verwendet die geladenen Text- und Effektgruppen. Wählt Aktivierungstexte zufällig aus und gibt Effekte über `play(effect)` aus. |
|
||||
| `EndCommand` | Öffentlicher Informationsbefehl `/end`, von EndPrevent registriert. Zeigt die Restzeit oder einen passenden Hinweis zum tatsächlichen Zustand. |
|
||||
|
||||
## YAML und dynamische Effekte
|
||||
|
||||
@@ -16,6 +18,7 @@
|
||||
times:
|
||||
end-open-at: "2026-11-15T18:00:00+01:00"
|
||||
outer-end-open-at: "2026-11-16T18:00:00+01:00"
|
||||
nether-open-at: null
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
@@ -56,10 +59,20 @@ Outer-End-Termin ist die Hauptinsel sofort verfügbar und das Outer End öffnet
|
||||
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.
|
||||
`nether-open-at` wird beim Reload als Nether-Termin angezeigt. Die automatische Nether-Steuerung
|
||||
ist weiterhin nicht angebunden.
|
||||
|
||||
## Gemeinsamer Zustandswechsel und Commands
|
||||
|
||||
`/end` ist für alle Spieler und die Konsole verfügbar. Bei geschlossenem End mit zukünftigem
|
||||
Termin lautet die Antwort beispielsweise: „Das End öffnet in 2 Tagen, 3 Stunden und 4 Minuten.“
|
||||
Angebrochene Minuten werden aufgerundet. Ist das End schon geöffnet, kein Termin gesetzt oder
|
||||
ein vergangener Termin bei weiterhin geschlossenem End vorhanden, wird dies entsprechend gemeldet.
|
||||
Der Informationsbefehl ändert keinen Zustand und schreibt keine Datei.
|
||||
|
||||
`/endPrevent preventEnd`, `/endPrevent allowEnd` und `/endPrevent onlyInnerEnd` bleiben bestehen.
|
||||
`/endPrevent` ohne Argument zeigt den Zustand. Status und Bestätigungen sind auf Deutsch in
|
||||
End-Grün formuliert; ungültige Argumente zeigen die Verwendung statt einer Erfolgsmeldung.
|
||||
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` alle Effekte mit
|
||||
@@ -75,6 +88,9 @@ gesetzten Zustand nicht auf; die Steuerung bleibt dann vollständig manuell.
|
||||
Manuelle Commands 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.
|
||||
Die Reload-Bestätigung zeigt End- und Nether-Termin als Datum mit Uhrzeit und Berliner Zeitzone,
|
||||
bei zukünftigen Terminen zusätzlich die Restzeit. Fehlende und vergangene Termine sind gekennzeichnet.
|
||||
Diese Zusammenfassung wird ausschließlich an den ausführenden CommandSender gesendet.
|
||||
|
||||
## Ankündigungen und Portalversuche
|
||||
|
||||
@@ -98,9 +114,12 @@ fällig, wird sie regulär ausgeführt. Dasselbe gilt für eine in diesem Zeitra
|
||||
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 zufälligen konfigurierten Text im Chat,
|
||||
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.
|
||||
abgebrochen. Aktivierungs- und Betretversuche gesperrter Endportale verwenden dieselbe Sperrprüfung
|
||||
und einen gemeinsamen Nachrichten-Cooldown: Nur der auslösende Spieler erhält einen zufälligen
|
||||
konfigurierten Text im Chat, höchstens einmal alle drei echten Sekunden. Versuche während des
|
||||
Cooldowns werden weiterhin abgebrochen; sie verlängern den Nachrichten-Cooldown nicht.
|
||||
Die Prüfung nutzt `System.nanoTime()`, benötigt keinen zusätzlichen Task und entfernt
|
||||
Spieler-Einträge beim Verlassen. Die Gateway-Meldung für das gesperrte Outer End bleibt getrennt davon.
|
||||
Alle Flavourtexte und zeitgesteuerten Chat-Effekte verwenden einheitlich End-Grün (`#80A71A`).
|
||||
|
||||
## Erweiterungen und Leistung
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
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.DimensionScheduleText;
|
||||
import eu.mhsl.craftattack.spawn.core.Main;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Public, read-only information command; the existing management command remains separate. */
|
||||
class EndCommand extends ApplianceCommand<EndPrevent> {
|
||||
public EndCommand() {
|
||||
super("end");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(args.length != 0) throw new Error("Verwendung: /end");
|
||||
EndPrevent end = this.getAppliance();
|
||||
if(!end.isEndClosed()) {
|
||||
sender.sendMessage(DimensionScheduleText.message(end.isOnlyInner()
|
||||
? "Das End ist bereits geöffnet. Das Outer End ist noch gesperrt."
|
||||
: "Das End ist bereits vollständig geöffnet."));
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Instant> openingAt = Main.instance().getAppliances().stream()
|
||||
.filter(DimensionScheduler.class::isInstance)
|
||||
.map(DimensionScheduler.class::cast)
|
||||
.findFirst()
|
||||
.flatMap(scheduler -> scheduler.getTime("end-open-at"));
|
||||
Instant now = Instant.now();
|
||||
String message;
|
||||
if(openingAt.isEmpty()) {
|
||||
message = "Für das End ist noch kein Öffnungstermin festgelegt.";
|
||||
} else if(!openingAt.get().isAfter(now)) {
|
||||
message = "Der Öffnungstermin ist bereits erreicht. Das End ist derzeit noch geschlossen.";
|
||||
} else {
|
||||
message = "Das End öffnet in " + DimensionScheduleText.remaining(openingAt.get(), now) + ".";
|
||||
}
|
||||
sender.sendMessage(DimensionScheduleText.message(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -2,8 +2,8 @@ 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.DimensionScheduleText;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -13,7 +13,6 @@ import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/** Presents YAML-defined End effects. Add sounds or other effects in play(). */
|
||||
final class EndOpeningEffects {
|
||||
private static final TextColor END_GREEN = TextColor.color(0x80A71A);
|
||||
private @Nullable DimensionScheduler scheduler;
|
||||
|
||||
void configure(DimensionScheduler scheduler) {
|
||||
@@ -29,7 +28,7 @@ final class EndOpeningEffects {
|
||||
List<String> messages = this.scheduler.getMessages("end-activation-denied");
|
||||
if(messages.isEmpty()) return;
|
||||
String message = messages.get(ThreadLocalRandom.current().nextInt(messages.size()));
|
||||
player.sendMessage(Component.text(message, END_GREEN));
|
||||
player.sendMessage(DimensionScheduleText.message(message));
|
||||
}
|
||||
|
||||
void playOpening() {
|
||||
@@ -39,7 +38,7 @@ final class EndOpeningEffects {
|
||||
}
|
||||
|
||||
void play(Effect effect) {
|
||||
Component message = Component.text(effect.message(), END_GREEN);
|
||||
Component message = DimensionScheduleText.message(effect.message());
|
||||
Bukkit.getOnlinePlayers().forEach(player -> player.sendMessage(message));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -101,6 +101,6 @@ public class EndPrevent extends Appliance {
|
||||
|
||||
@Override
|
||||
protected @NotNull List<ApplianceCommand<?>> commands() {
|
||||
return List.of(new EndPreventCommand());
|
||||
return List.of(new EndPreventCommand(), new EndCommand());
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -1,12 +1,10 @@
|
||||
package eu.mhsl.craftattack.spawn.common.appliances.tooling.endPrevent;
|
||||
|
||||
import eu.mhsl.craftattack.spawn.common.appliances.tooling.dimensionScheduler.DimensionScheduleText;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceCommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,19 +21,24 @@ class EndPreventCommand extends ApplianceCommand<EndPrevent> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
|
||||
if(args.length == 1 && this.arguments.containsKey(args[0])) {
|
||||
this.getAppliance().setEndState(this.arguments.get(args[0]));
|
||||
sender.sendMessage(Component.text("Setting updated!", NamedTextColor.GREEN));
|
||||
protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(args.length > 1 || (args.length == 1 && !this.arguments.containsKey(args[0]))) {
|
||||
throw new Error("Verwendung: /endPrevent [preventEnd|allowEnd|onlyInnerEnd]");
|
||||
}
|
||||
sender.sendMessage(Component.text(
|
||||
String.format("The End is now on '%s'!", this.getAppliance().getEndPreventState().name()),
|
||||
NamedTextColor.GOLD
|
||||
));
|
||||
if(args.length == 1) {
|
||||
this.getAppliance().setEndState(this.arguments.get(args[0]));
|
||||
}
|
||||
String message = switch(this.getAppliance().getEndPreventState()) {
|
||||
case CLOSED -> "Das End ist geschlossen.";
|
||||
case OPEN -> "Das End ist vollständig geöffnet.";
|
||||
case NO_OUTER -> "Die Hauptinsel des Ends ist geöffnet. Das Outer End ist gesperrt.";
|
||||
};
|
||||
sender.sendMessage(DimensionScheduleText.message(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
return this.arguments.keySet().stream().toList();
|
||||
public @NotNull List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||
if(args.length != 1) return List.of();
|
||||
return this.tabCompleteReducer(this.arguments.keySet().stream().toList(), args);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -4,7 +4,6 @@ import com.destroystokyo.paper.event.player.PlayerTeleportEndGatewayEvent;
|
||||
import eu.mhsl.craftattack.spawn.core.appliance.ApplianceListener;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
@@ -20,9 +19,11 @@ import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class EndPreventListener extends ApplianceListener<EndPrevent> {
|
||||
private final Map<UUID, Integer> lastDeniedTick = new HashMap<>();
|
||||
private static final long MESSAGE_COOLDOWN_NANOS = TimeUnit.SECONDS.toNanos(3);
|
||||
private final Map<UUID, Long> lastDeniedMessageAt = new HashMap<>();
|
||||
|
||||
@EventHandler
|
||||
public void onEnderEyeInteraction(PlayerInteractEvent event) {
|
||||
@@ -38,7 +39,7 @@ class EndPreventListener extends ApplianceListener<EndPrevent> {
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
this.lastDeniedTick.remove(event.getPlayer().getUniqueId());
|
||||
this.lastDeniedMessageAt.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -59,10 +60,11 @@ class EndPreventListener extends ApplianceListener<EndPrevent> {
|
||||
if(!this.getAppliance().isEndClosed()) return;
|
||||
event.setCancelled(true);
|
||||
|
||||
// Cancel every attempt, but show only one flavour text per player and server tick.
|
||||
int currentTick = Bukkit.getCurrentTick();
|
||||
Integer previousTick = this.lastDeniedTick.put(player.getUniqueId(), currentTick);
|
||||
if(previousTick != null && previousTick == currentTick) return;
|
||||
// Always block access. Only messages are throttled, using elapsed real time independent of TPS.
|
||||
long now = System.nanoTime();
|
||||
Long previousMessageAt = this.lastDeniedMessageAt.get(player.getUniqueId());
|
||||
if(previousMessageAt != null && now - previousMessageAt < MESSAGE_COOLDOWN_NANOS) return;
|
||||
this.lastDeniedMessageAt.put(player.getUniqueId(), now);
|
||||
this.getAppliance().effects().denyActivation(player);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
times:
|
||||
end-open-at: "2026-11-08T19:16:34+01:00"
|
||||
outer-end-open-at: null
|
||||
nether-open-at: null
|
||||
|
||||
messages:
|
||||
end-activation-denied:
|
||||
|
||||
Reference in new issue
Block a user