Add automatic restart feature

This commit is contained in:
Alessandro Proto 2024-10-27 16:02:39 +01:00
parent 8e7244b0b8
commit 6b99f27f89
9 changed files with 280 additions and 20 deletions

View file

@ -1,6 +1,7 @@
package cc.reconnected.server; package cc.reconnected.server;
import cc.reconnected.server.api.events.RccEvents; import cc.reconnected.server.api.events.RccEvents;
import cc.reconnected.server.commands.admin.*;
import cc.reconnected.server.commands.home.*; import cc.reconnected.server.commands.home.*;
import cc.reconnected.server.commands.misc.*; import cc.reconnected.server.commands.misc.*;
import cc.reconnected.server.commands.spawn.*; import cc.reconnected.server.commands.spawn.*;
@ -101,6 +102,7 @@ public class RccServer implements ModInitializer {
DeleteWarpCommand.register(dispatcher, registryAccess, environment); DeleteWarpCommand.register(dispatcher, registryAccess, environment);
TimeBarCommand.register(dispatcher, registryAccess, environment); TimeBarCommand.register(dispatcher, registryAccess, environment);
RestartCommand.register(dispatcher, registryAccess, environment);
NearCommand.register(dispatcher, registryAccess, environment); NearCommand.register(dispatcher, registryAccess, environment);
}); });
@ -111,6 +113,7 @@ public class RccServer implements ModInitializer {
TabList.register(); TabList.register();
HttpApiServer.register(); HttpApiServer.register();
BossBarManager.register(); BossBarManager.register();
AutoRestart.register();
ServerLifecycleEvents.SERVER_STARTED.register(server -> { ServerLifecycleEvents.SERVER_STARTED.register(server -> {
luckPerms = LuckPermsProvider.get(); luckPerms = LuckPermsProvider.get();

View file

@ -17,7 +17,7 @@ public class RccServerConfigModel {
public String afkTag = "<gray>[AFK]</gray> "; public String afkTag = "<gray>[AFK]</gray> ";
public String tellMessage = "<gold>[</gold><source> <gray>→</gray> <target><gold>]</gold> <message>"; public String tellMessage = "<gold>[</gold><source> <gray>→</gray> <target><gold>]</gold> <message>";
public String tellMessageSpy = "\uD83D\uDC41 <gray>[<source> → <target>]</gray> <message>"; public String tellMessageSpy = "\uD83D\uDC41 <gray>[<source> → <target>] <message></gray>";
public int teleportRequestTimeout = 120; public int teleportRequestTimeout = 120;
@ -38,8 +38,30 @@ public class RccServerConfigModel {
public int nearCommandDefaultRange = 32; public int nearCommandDefaultRange = 32;
public boolean enableAutoRestart = true; public boolean enableAutoRestart = true;
public String restartBarLabel = "Server restarting in <remaining_time>";
public String restartKickMessage = "The server is restarting!";
public String restartChatMessage = "<red>The server is restarting in </red><gold><remaining_time></gold>";
public ArrayList<String> restartAt = new ArrayList<>(List.of( public ArrayList<String> restartAt = new ArrayList<>(List.of(
"06:00", "06:00",
"18:00" "18:00"
)); ));
public String restartSound = "minecraft:block.note_block.bell";
public float restartSoundPitch = 0.9f;
public ArrayList<Integer> restartNotifications = new ArrayList<>(List.of(
600,
300,
120,
60,
30,
15,
10,
5,
4,
3,
2,
1
));
} }

View file

@ -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.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;

View file

@ -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.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;

View file

@ -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.RccServer;
import cc.reconnected.server.api.events.RccEvents; import cc.reconnected.server.api.events.RccEvents;

View file

@ -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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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<ServerCommandSource> 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;
}
}

View file

@ -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.api.events.BossBarEvents;
import cc.reconnected.server.core.BossBarManager; import cc.reconnected.server.core.BossBarManager;

View file

@ -1,17 +1,166 @@
package cc.reconnected.server.core; 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 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 { public class AutoRestart {
private static MinecraftServer server; private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public static void register() { private static BossBarManager.TimeBar restartBar = null;
ServerLifecycleEvents.SERVER_STARTING.register(s -> server = s); 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;
} }
} }

View file

@ -5,6 +5,7 @@ import cc.reconnected.server.api.events.BossBarEvents;
import cc.reconnected.server.util.Components; import cc.reconnected.server.util.Components;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; 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.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; 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) { 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(); 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) { public boolean cancelTimeBar(TimeBar timeBar) {
@ -128,18 +130,22 @@ public class BossBarManager {
} }
public void updateName() { public void updateName() {
var text = parseLabel(label);
bossBar.setName(Components.toText(text));
}
public Component parseLabel(String labelString) {
var totalTime = formatTime(this.time); var totalTime = formatTime(this.time);
var elapsedTime = formatTime(this.elapsedSeconds); var elapsedTime = formatTime(this.elapsedSeconds);
var remaining = time - elapsedSeconds; var remaining = getRemainingSeconds();
var remainingTime = formatTime(remaining); var remainingTime = formatTime(remaining);
var text = miniMessage.deserialize(label, TagResolver.resolver(
return miniMessage.deserialize(labelString, TagResolver.resolver(
Placeholder.parsed("total_time", totalTime), Placeholder.parsed("total_time", totalTime),
Placeholder.parsed("elapsed_time", elapsedTime), Placeholder.parsed("elapsed_time", elapsedTime),
Placeholder.parsed("remaining_time", remainingTime) Placeholder.parsed("remaining_time", remainingTime)
)); ));
bossBar.setName(Components.toText(text));
} }
public UUID getUuid() { public UUID getUuid() {
@ -162,6 +168,10 @@ public class BossBarManager {
return elapsedSeconds; return elapsedSeconds;
} }
public int getRemainingSeconds() {
return time - elapsedSeconds;
}
public boolean isCountdown() { public boolean isCountdown() {
return countdown; return countdown;
} }