From 6b99f27f89b9ecb9b71c4dd4355a3f84d3400fb8 Mon Sep 17 00:00:00 2001 From: Alessandro Proto Date: Sun, 27 Oct 2024 16:02:39 +0100 Subject: [PATCH] Add automatic restart feature --- .../java/cc/reconnected/server/RccServer.java | 3 + .../server/RccServerConfigModel.java | 24 ++- .../commands/{misc => admin}/FlyCommand.java | 2 +- .../commands/{misc => admin}/GodCommand.java | 2 +- .../commands/{misc => admin}/RccCommand.java | 2 +- .../server/commands/admin/RestartCommand.java | 76 +++++++++ .../{misc => admin}/TimeBarCommand.java | 2 +- .../reconnected/server/core/AutoRestart.java | 161 +++++++++++++++++- .../server/core/BossBarManager.java | 28 ++- 9 files changed, 280 insertions(+), 20 deletions(-) rename src/main/java/cc/reconnected/server/commands/{misc => admin}/FlyCommand.java (98%) rename src/main/java/cc/reconnected/server/commands/{misc => admin}/GodCommand.java (98%) rename src/main/java/cc/reconnected/server/commands/{misc => admin}/RccCommand.java (97%) create mode 100644 src/main/java/cc/reconnected/server/commands/admin/RestartCommand.java rename src/main/java/cc/reconnected/server/commands/{misc => admin}/TimeBarCommand.java (99%) diff --git a/src/main/java/cc/reconnected/server/RccServer.java b/src/main/java/cc/reconnected/server/RccServer.java index 6a1bd3c..21dbd82 100644 --- a/src/main/java/cc/reconnected/server/RccServer.java +++ b/src/main/java/cc/reconnected/server/RccServer.java @@ -1,6 +1,7 @@ package cc.reconnected.server; import cc.reconnected.server.api.events.RccEvents; +import cc.reconnected.server.commands.admin.*; import cc.reconnected.server.commands.home.*; import cc.reconnected.server.commands.misc.*; import cc.reconnected.server.commands.spawn.*; @@ -101,6 +102,7 @@ public class RccServer implements ModInitializer { DeleteWarpCommand.register(dispatcher, registryAccess, environment); TimeBarCommand.register(dispatcher, registryAccess, environment); + RestartCommand.register(dispatcher, registryAccess, environment); NearCommand.register(dispatcher, registryAccess, environment); }); @@ -111,6 +113,7 @@ public class RccServer implements ModInitializer { TabList.register(); HttpApiServer.register(); BossBarManager.register(); + AutoRestart.register(); ServerLifecycleEvents.SERVER_STARTED.register(server -> { luckPerms = LuckPermsProvider.get(); diff --git a/src/main/java/cc/reconnected/server/RccServerConfigModel.java b/src/main/java/cc/reconnected/server/RccServerConfigModel.java index 9798bce..e06c020 100644 --- a/src/main/java/cc/reconnected/server/RccServerConfigModel.java +++ b/src/main/java/cc/reconnected/server/RccServerConfigModel.java @@ -17,7 +17,7 @@ public class RccServerConfigModel { public String afkTag = "[AFK] "; public String tellMessage = "[ ] "; - public String tellMessageSpy = "\uD83D\uDC41 [] "; + public String tellMessageSpy = "\uD83D\uDC41 [] "; public int teleportRequestTimeout = 120; @@ -38,8 +38,30 @@ public class RccServerConfigModel { public int nearCommandDefaultRange = 32; public boolean enableAutoRestart = true; + public String restartBarLabel = "Server restarting in "; + public String restartKickMessage = "The server is restarting!"; + public String restartChatMessage = "The server is restarting in "; + public ArrayList restartAt = new ArrayList<>(List.of( "06:00", "18:00" )); + + public String restartSound = "minecraft:block.note_block.bell"; + public float restartSoundPitch = 0.9f; + + public ArrayList restartNotifications = new ArrayList<>(List.of( + 600, + 300, + 120, + 60, + 30, + 15, + 10, + 5, + 4, + 3, + 2, + 1 + )); } diff --git a/src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java b/src/main/java/cc/reconnected/server/commands/admin/FlyCommand.java similarity index 98% rename from src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java rename to src/main/java/cc/reconnected/server/commands/admin/FlyCommand.java index 97198bb..83e4bae 100644 --- a/src/main/java/cc/reconnected/server/commands/misc/FlyCommand.java +++ b/src/main/java/cc/reconnected/server/commands/admin/FlyCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands.misc; +package cc.reconnected.server.commands.admin; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; diff --git a/src/main/java/cc/reconnected/server/commands/misc/GodCommand.java b/src/main/java/cc/reconnected/server/commands/admin/GodCommand.java similarity index 98% rename from src/main/java/cc/reconnected/server/commands/misc/GodCommand.java rename to src/main/java/cc/reconnected/server/commands/admin/GodCommand.java index 969c8eb..64f0c43 100644 --- a/src/main/java/cc/reconnected/server/commands/misc/GodCommand.java +++ b/src/main/java/cc/reconnected/server/commands/admin/GodCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands.misc; +package cc.reconnected.server.commands.admin; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; diff --git a/src/main/java/cc/reconnected/server/commands/misc/RccCommand.java b/src/main/java/cc/reconnected/server/commands/admin/RccCommand.java similarity index 97% rename from src/main/java/cc/reconnected/server/commands/misc/RccCommand.java rename to src/main/java/cc/reconnected/server/commands/admin/RccCommand.java index ca47643..a85a65a 100644 --- a/src/main/java/cc/reconnected/server/commands/misc/RccCommand.java +++ b/src/main/java/cc/reconnected/server/commands/admin/RccCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands.misc; +package cc.reconnected.server.commands.admin; import cc.reconnected.server.RccServer; import cc.reconnected.server.api.events.RccEvents; diff --git a/src/main/java/cc/reconnected/server/commands/admin/RestartCommand.java b/src/main/java/cc/reconnected/server/commands/admin/RestartCommand.java new file mode 100644 index 0000000..f893260 --- /dev/null +++ b/src/main/java/cc/reconnected/server/commands/admin/RestartCommand.java @@ -0,0 +1,76 @@ +package cc.reconnected.server.commands.admin; + +import cc.reconnected.server.RccServer; +import cc.reconnected.server.api.events.RccEvents; +import cc.reconnected.server.core.AutoRestart; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; + +import static net.minecraft.server.command.CommandManager.*; + +public class RestartCommand { + public static void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + var rootCommand = literal("restart") + .requires(Permissions.require("rcc.command.restart", 4)) + .then(literal("schedule") + .then(argument("seconds", IntegerArgumentType.integer(0)) + .executes(context -> schedule(context, IntegerArgumentType.getInteger(context, "seconds"), null)) + .then(argument("message", StringArgumentType.greedyString()) + .executes(context -> schedule(context, IntegerArgumentType.getInteger(context, "seconds"), StringArgumentType.getString(context, "message"))))) + .then(literal("next") + .executes(RestartCommand::scheduleNext)) + ) + .then(literal("cancel") + .executes(RestartCommand::cancel)); + + dispatcher.register(rootCommand); + } + + private static int schedule(CommandContext context, int seconds, @Nullable String message) { + if (message == null) { + message = RccServer.CONFIG.restartBarLabel(); + } + AutoRestart.schedule(seconds, message); + + context.getSource().sendFeedback(() -> Text.of("Manual restart scheduled in " + seconds + " seconds."), true); + + return 1; + } + + private static int scheduleNext(CommandContext context) { + if (AutoRestart.isScheduled()) { + context.getSource().sendFeedback(() -> Text.literal("There is already a scheduled restart.").formatted(Formatting.RED), false); + return 1; + } + + var delay = AutoRestart.scheduleNextRestart(); + + if (delay == null) { + context.getSource().sendFeedback(() -> Text.literal("Could not schedule next automatic restart.").formatted(Formatting.RED), false); + } else { + context.getSource().sendFeedback(() -> Text.literal("Next automatic restart scheduled in " + delay + " seconds."), true); + } + + return 1; + } + + private static int cancel(CommandContext context) { + if (!AutoRestart.isScheduled()) { + context.getSource().sendFeedback(() -> Text.literal("There is no scheduled restart.").formatted(Formatting.RED), false); + return 1; + } + + AutoRestart.cancel(); + context.getSource().sendFeedback(() -> Text.literal("Restart schedule canceled."), true); + return 1; + } +} diff --git a/src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java b/src/main/java/cc/reconnected/server/commands/admin/TimeBarCommand.java similarity index 99% rename from src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java rename to src/main/java/cc/reconnected/server/commands/admin/TimeBarCommand.java index a3eff3e..e30f389 100644 --- a/src/main/java/cc/reconnected/server/commands/misc/TimeBarCommand.java +++ b/src/main/java/cc/reconnected/server/commands/admin/TimeBarCommand.java @@ -1,4 +1,4 @@ -package cc.reconnected.server.commands.misc; +package cc.reconnected.server.commands.admin; import cc.reconnected.server.api.events.BossBarEvents; import cc.reconnected.server.core.BossBarManager; diff --git a/src/main/java/cc/reconnected/server/core/AutoRestart.java b/src/main/java/cc/reconnected/server/core/AutoRestart.java index f003091..ef6f01b 100644 --- a/src/main/java/cc/reconnected/server/core/AutoRestart.java +++ b/src/main/java/cc/reconnected/server/core/AutoRestart.java @@ -1,17 +1,166 @@ package cc.reconnected.server.core; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import cc.reconnected.server.RccServer; +import cc.reconnected.server.api.events.BossBarEvents; +import cc.reconnected.server.api.events.RccEvents; +import cc.reconnected.server.util.Components; +import net.kyori.adventure.key.InvalidKeyException; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.minecraft.entity.boss.BossBar; import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.Nullable; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; public class AutoRestart { - private static MinecraftServer server; - public static void register() { - ServerLifecycleEvents.SERVER_STARTING.register(s -> server = s); + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private static BossBarManager.TimeBar restartBar = null; + private static Key notificationKey; + private static ScheduledFuture currentSchedule = null; - + + public static void register() { + + var miniMessage = MiniMessage.miniMessage(); + + RccEvents.READY.register((server, luckPerms) -> { + if (RccServer.CONFIG.enableAutoRestart()) { + scheduleNextRestart(); + } + }); + + BossBarEvents.PROGRESS.register((timeBar, server) -> { + if (restartBar == null || !timeBar.getUuid().equals(restartBar.getUuid())) + return; + + var notificationTimes = RccServer.CONFIG.restartNotifications(); + + var remainingSeconds = restartBar.getRemainingSeconds(); + if (notificationTimes.contains(remainingSeconds)) { + notifyRestart(server, restartBar); + } + + }); + + // Shutdown + BossBarEvents.END.register((timeBar, server) -> { + if (restartBar == null || !timeBar.getUuid().equals(restartBar.getUuid())) + return; + + final var text = Components.toText( + miniMessage.deserialize(RccServer.CONFIG.restartKickMessage()) + ); + server.getPlayerManager().getPlayerList().forEach(player -> { + player.networkHandler.disconnect(text); + }); + scheduler.shutdownNow(); + server.stop(false); + }); + + setup(); + + RccEvents.RELOAD.register(instance -> { + setup(); + }); } - private static void schedule() { + private static void setup() { + var soundName = RccServer.CONFIG.restartSound(); + try { + notificationKey = Key.key(soundName); + } catch (InvalidKeyException e) { + RccServer.LOGGER.error("Invalid restart notification sound name", e); + notificationKey = Key.key("minecraft", "block.note_block.bell"); + } + } + public static void schedule(int seconds, String message) { + restartBar = BossBarManager.getInstance().startTimeBar( + message, + seconds, + BossBar.Color.RED, + BossBar.Style.NOTCHED_20, + true + ); + } + + public static boolean isScheduled() { + return restartBar != null || currentSchedule != null && !currentSchedule.isCancelled(); + } + + public static void cancel() { + if (restartBar != null) { + BossBarManager.getInstance().cancelTimeBar(restartBar); + restartBar = null; + } + + if(currentSchedule != null) { + currentSchedule.cancel(false); + currentSchedule = null; + } + } + + private static void notifyRestart(MinecraftServer server, BossBarManager.TimeBar bar) { + var rcc = RccServer.getInstance(); + var audience = rcc.adventure().players(); + var sound = Sound.sound(notificationKey, Sound.Source.MASTER, 10f, RccServer.CONFIG.restartSoundPitch()); + audience.playSound(sound, Sound.Emitter.self()); + + var comp = bar.parseLabel(RccServer.CONFIG.restartChatMessage()); + rcc.broadcastMessage(server, comp); + } + + @Nullable + public static Long scheduleNextRestart() { + var delay = getNextDelay(); + if (delay == null) + return null; + + var barTime = 10 * 60; + // start bar 10 mins earlier + var barStartTime = delay - barTime; + + currentSchedule = scheduler.schedule(() -> { + schedule(barTime, RccServer.CONFIG.restartBarLabel()); + }, barStartTime, TimeUnit.SECONDS); + + RccServer.LOGGER.info("Restart scheduled for in {} seconds", delay); + return delay; + } + + @Nullable + private static Long getNextDelay() { + var restartTimeStrings = RccServer.CONFIG.restartAt(); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRunTime = null; + long shortestDelay = Long.MAX_VALUE; + + for (var timeString : restartTimeStrings) { + LocalTime targetTime = LocalTime.parse(timeString); + LocalDateTime targetDateTime = now.with(targetTime); + + if (targetDateTime.isBefore(now)) { + targetDateTime = targetDateTime.plusDays(1); + } + + long delay = Duration.between(now, targetDateTime).toSeconds(); + if (delay < shortestDelay) { + shortestDelay = delay; + nextRunTime = targetDateTime; + } + } + + if (nextRunTime != null) { + return shortestDelay; + } + return null; } } diff --git a/src/main/java/cc/reconnected/server/core/BossBarManager.java b/src/main/java/cc/reconnected/server/core/BossBarManager.java index 52b6a89..8ea96a1 100644 --- a/src/main/java/cc/reconnected/server/core/BossBarManager.java +++ b/src/main/java/cc/reconnected/server/core/BossBarManager.java @@ -5,6 +5,7 @@ import cc.reconnected.server.api.events.BossBarEvents; import cc.reconnected.server.util.Components; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; @@ -67,16 +68,17 @@ public class BossBarManager { } public TimeBar startTimeBar(String label, int seconds, BossBar.Color color, BossBar.Style style, boolean countdown) { - var countdownBar = new TimeBar(label, seconds, countdown, color, style); + var timeBar = new TimeBar(label, seconds, countdown, color, style); - timeBars.add(countdownBar); + timeBars.add(timeBar); var players = server.getPlayerManager().getPlayerList(); - showBar(players, countdownBar); + showBar(players, timeBar); - BossBarEvents.START.invoker().onStart(countdownBar, server); + BossBarEvents.START.invoker().onStart(timeBar, server); + BossBarEvents.PROGRESS.invoker().onProgress(timeBar, server); - return countdownBar; + return timeBar; } public boolean cancelTimeBar(TimeBar timeBar) { @@ -128,18 +130,22 @@ public class BossBarManager { } public void updateName() { + var text = parseLabel(label); + bossBar.setName(Components.toText(text)); + } + + public Component parseLabel(String labelString) { var totalTime = formatTime(this.time); var elapsedTime = formatTime(this.elapsedSeconds); - var remaining = time - elapsedSeconds; + var remaining = getRemainingSeconds(); var remainingTime = formatTime(remaining); - var text = miniMessage.deserialize(label, TagResolver.resolver( + + return miniMessage.deserialize(labelString, TagResolver.resolver( Placeholder.parsed("total_time", totalTime), Placeholder.parsed("elapsed_time", elapsedTime), Placeholder.parsed("remaining_time", remainingTime) )); - - bossBar.setName(Components.toText(text)); } public UUID getUuid() { @@ -162,6 +168,10 @@ public class BossBarManager { return elapsedSeconds; } + public int getRemainingSeconds() { + return time - elapsedSeconds; + } + public boolean isCountdown() { return countdown; }