diff --git a/gradle.properties b/gradle.properties index 85ec5b8..ce33bca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group=com.mineinabyss version=0.12 -idofrontVersion=1.0.3 +idofrontVersion=1.0.5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d560e90..3ac6f72 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -gearyPaper = "0.33.2" +gearyPaper = "0.33.14" chatty = "0.9.1" [libraries] diff --git a/src/main/kotlin/com/mineinabyss/extracommands/ExtraCommands.kt b/src/main/kotlin/com/mineinabyss/extracommands/ExtraCommands.kt index 7108489..e8f11db 100644 --- a/src/main/kotlin/com/mineinabyss/extracommands/ExtraCommands.kt +++ b/src/main/kotlin/com/mineinabyss/extracommands/ExtraCommands.kt @@ -1,10 +1,12 @@ package com.mineinabyss.extracommands +import com.mineinabyss.extracommands.dailyrestarts.RestartManager import com.mineinabyss.extracommands.listeners.AfkListener import com.mineinabyss.extracommands.listeners.GodListener import com.mineinabyss.extracommands.listeners.HuskHomesListener import com.mineinabyss.extracommands.listeners.SeenListener import com.mineinabyss.extracommands.listeners.VanishListener +import com.mineinabyss.idofront.config.config import com.mineinabyss.idofront.di.DI import com.mineinabyss.idofront.plugin.Plugins import com.mineinabyss.idofront.plugin.listeners @@ -16,6 +18,8 @@ class ExtraCommands : JavaPlugin() { createExtraCommandsContext() ExtraBrigadierCommands.registerCommands() + extraCommands.restartManager.scheduleDailyRestartIfEnabled() + listeners( AfkListener(), GodListener(), @@ -32,7 +36,8 @@ class ExtraCommands : JavaPlugin() { DI.remove() DI.add(object : ExtraCommandContext { override val plugin = this@ExtraCommands - override val config = ExtraConfig() + override val config by config("config", dataFolder.toPath(), ExtraConfig()) + override val restartManager: RestartManager = RestartManager(config.dailyRestarts) }) } } diff --git a/src/main/kotlin/com/mineinabyss/extracommands/ExtraConfig.kt b/src/main/kotlin/com/mineinabyss/extracommands/ExtraConfig.kt index 49cd442..c3e0afa 100644 --- a/src/main/kotlin/com/mineinabyss/extracommands/ExtraConfig.kt +++ b/src/main/kotlin/com/mineinabyss/extracommands/ExtraConfig.kt @@ -1,5 +1,6 @@ package com.mineinabyss.extracommands +import com.mineinabyss.extracommands.dailyrestarts.DailyRestartsConfig import com.mineinabyss.idofront.serialization.DurationSerializer import kotlinx.serialization.Serializable import kotlin.time.Duration @@ -7,7 +8,8 @@ import kotlin.time.Duration.Companion.minutes @Serializable data class ExtraConfig( - val afk: AfkConfig = AfkConfig() + val afk: AfkConfig = AfkConfig(), + val dailyRestarts: DailyRestartsConfig = DailyRestartsConfig(), ) { @Serializable data class AfkConfig( diff --git a/src/main/kotlin/com/mineinabyss/extracommands/ExtraContext.kt b/src/main/kotlin/com/mineinabyss/extracommands/ExtraContext.kt index a2b4f5d..2e4b232 100644 --- a/src/main/kotlin/com/mineinabyss/extracommands/ExtraContext.kt +++ b/src/main/kotlin/com/mineinabyss/extracommands/ExtraContext.kt @@ -1,9 +1,11 @@ package com.mineinabyss.extracommands +import com.mineinabyss.extracommands.dailyrestarts.RestartManager import com.mineinabyss.idofront.di.DI val extraCommands by DI.observe() interface ExtraCommandContext { val plugin: ExtraCommands val config: ExtraConfig + val restartManager: RestartManager } diff --git a/src/main/kotlin/com/mineinabyss/extracommands/commands/ScheduleRestartCommand.kt b/src/main/kotlin/com/mineinabyss/extracommands/commands/ScheduleRestartCommand.kt index 0233d0d..3ceff99 100644 --- a/src/main/kotlin/com/mineinabyss/extracommands/commands/ScheduleRestartCommand.kt +++ b/src/main/kotlin/com/mineinabyss/extracommands/commands/ScheduleRestartCommand.kt @@ -1,113 +1,43 @@ package com.mineinabyss.extracommands.commands -import com.github.shynixn.mccoroutine.bukkit.launch import com.mineinabyss.extracommands.extraCommands -import com.mineinabyss.geary.papermc.datastore.encodeComponentsTo -import com.mineinabyss.geary.papermc.tracking.entities.toGearyOrNull +import com.mineinabyss.extracommands.dailyrestarts.RestartManager import com.mineinabyss.idofront.commands.brigadier.RootIdoCommands import com.mineinabyss.idofront.commands.brigadier.arguments.DurationTypeArgument import com.mineinabyss.idofront.commands.brigadier.executes -import com.mineinabyss.idofront.plugin.Plugins -import com.mineinabyss.idofront.textcomponents.miniMsg -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import net.kyori.adventure.title.Title -import org.bukkit.Bukkit -import java.time.Duration.ofSeconds -import kotlin.time.Duration +import com.mineinabyss.idofront.messaging.error +import com.mineinabyss.idofront.messaging.success import kotlin.time.Duration.Companion.seconds -//val maintenance by lazy { runCatching { MaintenanceProvider.get() as MaintenanceProxy }.getOrNull() } -fun RootIdoCommands.scheduleRestartCommand() { - var currentJob: Job? = null +fun RootIdoCommands.scheduleRestartCommand( + service: RestartManager = extraCommands.restartManager, +) { "schedulestop" { executes(DurationTypeArgument(10.seconds)) { duration -> - fun showTitle(time: Duration, fade: Boolean) { - Bukkit.getServer().showTitle( - Title.title( - "Server Stopping".miniMsg(), - "Server will stop in ${time.toComponents { d, h, m, s, _ -> "${d}d ${h}h ${m}m ${s}s" }.replace("0[a-zA-Z]\\s? ".toRegex(), "")}.".miniMsg(), - if (fade) Title.Times.times(ofSeconds(1), ofSeconds(5), ofSeconds(1)) - else Title.Times.times(ofSeconds(0), ofSeconds(2), ofSeconds(0)) - ) - ) - } - currentJob = extraCommands.plugin.launch { - showTitle(duration, fade = true) - - (duration - 10.seconds).takeIf(Duration::isPositive)?.let { delay(it) } - - repeat(10) { - showTitle((10 - it).seconds, fade = false) - delay(1.seconds) - } - - //val server = maintenance?.getServer("survival") - //val isMaintenance = maintenance?.isMaintenance(server) ?: false - //if (!isMaintenance) maintenance?.setMaintenanceToServer(server, true) - Bukkit.savePlayers() - Bukkit.getWorlds().forEach { world -> - if (Plugins.isEnabled("Geary")) world.entities.forEach { e -> - e.toGearyOrNull()?.encodeComponentsTo(e.persistentDataContainer) - } - world.save() - } - //if (!isMaintenance) maintenance?.setMaintenanceToServer(server, false) - - Bukkit.shutdown() - } + service.scheduleStop(showTitleAtStart = true, duration) } } "schedulerestart" { executes(DurationTypeArgument(10.seconds)) { duration -> - fun showTitle(time: Duration, fade: Boolean) { - Bukkit.getServer().showTitle( - Title.title( - "Server Restarting".miniMsg(), - "Server will restart in ${time.toComponents { d, h, m, s, _ -> "${d}d ${h}h ${m}m ${s}s" }.replace("0[a-zA-Z]\\s? ".toRegex(), "")}.".miniMsg(), - if (fade) Title.Times.times(ofSeconds(1), ofSeconds(5), ofSeconds(1)) - else Title.Times.times(ofSeconds(0), ofSeconds(2), ofSeconds(0)) - ) - ) - } - currentJob = extraCommands.plugin.launch { - showTitle(duration, fade = true) - - (duration - 10.seconds).takeIf(Duration::isPositive)?.let { delay(it) } - - repeat(10) { - showTitle((10 - it).seconds, fade = false) - delay(1.seconds) - } - - //val server = maintenance?.getServer("survival") - //val isMaintenance = maintenance?.isMaintenance(server) ?: false - //if (!isMaintenance) maintenance?.setMaintenanceToServer(server, true) - Bukkit.savePlayers() - Bukkit.getWorlds().forEach { world -> - if (Plugins.isEnabled("Geary")) world.entities.forEach { e -> - e.toGearyOrNull()?.encodeComponentsTo(e.persistentDataContainer) - } - world.save() - } - //if (!isMaintenance) maintenance?.setMaintenanceToServer(server, false) - - Bukkit.spigot().restart() - } + service.scheduleRestart(showTitleAtStart = true, duration) } } "cancelrestart" { requiresPermission("extracommands.cancelrestart") + + "daily" { + requiresPermission("extracommands.cancelrestart.daily") + executes { + if (service.cancelDailyJob()) + sender.success("Cancelled daily restart.") + else sender.error("No daily restart scheduled, nothing to cancel.") + } + } + executes { - currentJob?.cancel() ?: return@executes - Bukkit.getServer().clearTitle() - Bukkit.getServer().showTitle( - Title.title( - "Server Restart Cancelled!".miniMsg(), - "Server will no longer restart...".miniMsg(), - Title.Times.times(ofSeconds(1), ofSeconds(5), ofSeconds(1)) - ) - ) + if (service.cancelJob()) + sender.success("Cancelled scheduled restart.") + else sender.error("No restart scheduled, nothing to cancel.") } } } diff --git a/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/DailyRestartsConfig.kt b/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/DailyRestartsConfig.kt new file mode 100644 index 0000000..8597d39 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/DailyRestartsConfig.kt @@ -0,0 +1,15 @@ +package com.mineinabyss.extracommands.dailyrestarts + +import com.mineinabyss.idofront.serialization.DurationSerializer +import kotlinx.serialization.Serializable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours + +@Serializable +data class DailyRestartsConfig( + val enabled: Boolean = false, + val skipIfUptimeLessThan: @Serializable(with = DurationSerializer::class) Duration = 4.hours, + val hour: Int = 0, + val minute: Int = 0, + val timeZone: String? = null, +) \ No newline at end of file diff --git a/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/RestartManager.kt b/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/RestartManager.kt new file mode 100644 index 0000000..f4502a5 --- /dev/null +++ b/src/main/kotlin/com/mineinabyss/extracommands/dailyrestarts/RestartManager.kt @@ -0,0 +1,169 @@ +package com.mineinabyss.extracommands.dailyrestarts + +import com.github.shynixn.mccoroutine.bukkit.launch +import com.mineinabyss.extracommands.extraCommands +import com.mineinabyss.geary.papermc.datastore.encodeComponentsTo +import com.mineinabyss.geary.papermc.tracking.entities.toGearyOrNull +import com.mineinabyss.idofront.plugin.Plugins +import com.mineinabyss.idofront.textcomponents.miniMsg +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import net.kyori.adventure.title.Title +import org.bukkit.Bukkit +import java.time.Duration +import java.time.ZoneId +import java.time.ZonedDateTime +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.toKotlinDuration + +class RestartManager( + val config: DailyRestartsConfig, +) { + var dailyRestartJob: Job? = null + var currentJob: Job? = null + + fun scheduleDailyRestartIfEnabled() { + if (!config.enabled) return + + val now = ZonedDateTime.now(runCatching { ZoneId.of(config.timeZone) }.getOrDefault(ZoneId.systemDefault())) + val nextRun = now + .withHour(config.hour) + .withMinute(config.minute) + .withSecond(0) + .let { if (now > it) it.plusDays(1) else it } + .let { nextTime -> + val timeUntilNextRun = Duration.between(now, nextTime) + if (timeUntilNextRun.toKotlinDuration() < config.skipIfUptimeLessThan) + nextTime.plusDays(1) + else nextTime + } + val delay = Duration.between(now, nextRun).toKotlinDuration() + println("Scheduling daily restart at $nextRun (in $delay)") + dailyRestartJob?.cancel() + dailyRestartJob = restartJob(showTitleAtStart = false, delay) + } + + fun showTitle( + fade: Boolean, + title: String, + subtitle: String, + ) { + Bukkit.getServer().showTitle( + Title.title( + title.miniMsg(), + subtitle.miniMsg(), + if (fade) Title.Times.times(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofSeconds(1)) + else Title.Times.times(Duration.ofSeconds(0), Duration.ofSeconds(2), Duration.ofSeconds(0)) + ) + ) + } + + fun delayToString(duration: kotlin.time.Duration) = duration + .toString() + + fun scheduleStop(showTitleAtStart: Boolean, duration: kotlin.time.Duration) { + currentJob?.cancel() + currentJob = scheduleJob( + showTitleAtStart = showTitleAtStart, + duration = duration, + title = { "Server Stopping" }, + subtitle = { "Server will stop in ${delayToString(it)}." }, + earlyWarning = { "[Server] stopping in ${delayToString(it)}." }, + run = { Bukkit.shutdown() } + ) + } + + private fun restartJob(showTitleAtStart: Boolean, duration: kotlin.time.Duration) = scheduleJob( + showTitleAtStart = showTitleAtStart, + duration = duration, + title = { "Server Restarting" }, + subtitle = { "Server will restart in ${delayToString(it)}." }, + earlyWarning = { "[Server] restarting in ${delayToString(it)}." }, + run = { Bukkit.restart() } + ) + + fun scheduleRestart(showTitleAtStart: Boolean, duration: kotlin.time.Duration) { + currentJob?.cancel() + currentJob = restartJob(showTitleAtStart, duration) + } + + fun cancelJob(): Boolean { + currentJob?.cancel() + val present = currentJob != null + currentJob = null + if (present) { + Bukkit.getServer().clearTitle() + Bukkit.getServer().showTitle( + Title.title( + "Server Restart Cancelled!".miniMsg(), + "Server will no longer restart...".miniMsg(), + Title.Times.times(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofSeconds(1)) + ) + ) + } + return present + } + + fun cancelDailyJob(): Boolean { + dailyRestartJob?.cancel() + val present = dailyRestartJob != null + dailyRestartJob = null + return present + } + + private fun scheduleJob( + showTitleAtStart: Boolean, + duration: kotlin.time.Duration, + earlyWarning: (kotlin.time.Duration) -> String, + title: (kotlin.time.Duration) -> String, + subtitle: (kotlin.time.Duration) -> String, + run: suspend () -> Unit, + ): Job = extraCommands.plugin.launch { + if (showTitleAtStart) showTitle(fade = true, title(duration), subtitle(duration)) + var rem = duration + + suspend fun mark(time: kotlin.time.Duration, exec: suspend () -> Unit) { + if (rem >= time) { + delay(rem - time) + rem = time + exec() + } + } + + mark(30.minutes) { + Bukkit.getServer().sendMessage(earlyWarning(rem).miniMsg()) + } + + mark(10.minutes) { + Bukkit.getServer().sendMessage(earlyWarning(rem).miniMsg()) + } + + mark(1.minutes) { + Bukkit.getServer().sendMessage(earlyWarning(rem).miniMsg()) + } + + mark(30.seconds) { + Bukkit.getServer().sendMessage(earlyWarning(rem).miniMsg()) + } + + + // Always show last 10 second countdown + (rem - 10.seconds).takeIf(kotlin.time.Duration::isPositive)?.let { delay(it) } + repeat(10) { + val left = (10 - it).seconds + showTitle(fade = false, title(left), subtitle(left)) + delay(1.seconds) + } + + Bukkit.savePlayers() + Bukkit.getWorlds().forEach { world -> + if (Plugins.isEnabled("Geary")) world.entities.forEach { e -> + e.toGearyOrNull()?.encodeComponentsTo(e.persistentDataContainer) + } + world.save() + } + + run() + } +} \ No newline at end of file