diff --git a/ansible/build.gradle.kts b/ansible/build.gradle.kts index f5d77db1c..71836bf84 100644 --- a/ansible/build.gradle.kts +++ b/ansible/build.gradle.kts @@ -38,6 +38,8 @@ dependencies { pvpPlugin(project(path = ":plugins:namelayer-paper")) pvpPlugin(project(path = ":plugins:civchat2-paper")) pvpPlugin(project(path = ":plugins:namecolors-paper")) + + proxyPlugin(project(path = ":plugins:announcements-velocity", configuration = "shadow")) } val copyPaperPlugins = tasks.register("copyPaperPlugins") { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5018afda2..470f8f362 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ paper = "1.21.3-R0.1-SNAPSHOT" junit = "5.8.2" nuotifier = "2.7.2" +velocity = "3.4.0-SNAPSHOT" [plugins] paper-userdev = { id = "io.papermc.paperweight.userdev", version = "2.0.0-beta.11" } @@ -10,6 +11,7 @@ runpaper = { id = "xyz.jpenilla.run-paper", version = "2.3.1" } [libraries] paper-api = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } +velocity-api = { group = "com.velocitypowered", name = "velocity-api", version.ref = "velocity"} aikar-acf = { group = "co.aikar", name = "acf-bukkit", version = "0.5.1-SNAPSHOT" } aikar-taskchain = { group = "co.aikar", name = "taskchain-bukkit", version = "3.7.2" } @@ -18,6 +20,9 @@ commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version commons-collections4 = { group = "org.apache.commons", name = "commons-collections4", version = "4.4" } commons-math3 = { group = "org.apache.commons", name = "commons-math3", version = "3.6.1" } +cron-utils = { group = "com.cronutils", name = "cron-utils", version = "9.2.1" } +snakeyaml = { group = "org.yaml", name = "snakeyaml", version = "2.3"} + rabbitmq-client = { group = "com.rabbitmq", name = "amqp-client", version = "5.17.1" } nuvotifier-api = { group = "com.github.NuVotifier.NuVotifier", name = "nuvotifier-api", version.ref = "nuotifier" } diff --git a/plugins/announcements-velocity/build.gradle.kts b/plugins/announcements-velocity/build.gradle.kts new file mode 100644 index 000000000..8826f99e3 --- /dev/null +++ b/plugins/announcements-velocity/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("com.github.johnrengelman.shadow") +} + +version = "1.0.0" + +dependencies { + compileOnly(libs.velocity.api) + annotationProcessor(libs.velocity.api) + + implementation(libs.cron.utils) + implementation(libs.snakeyaml) +} diff --git a/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsConfig.java b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsConfig.java new file mode 100644 index 000000000..ef81226ba --- /dev/null +++ b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsConfig.java @@ -0,0 +1,36 @@ +package net.civmc.announcements; + +import java.util.List; + +/** + * @implNote property must be named the same as in the config file, must also have getters and setters else snakeyaml doesn't like it + */ +public class AnnouncementsConfig { + + public static class ScheduledAnnouncement { + private String cron; + private String message; + + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + public String getCron() { + return cron; + } + public void setCron(String cron) { + this.cron = cron; + } + } + + private List scheduledAnnouncements; + + public List getScheduledAnnouncements() { + return scheduledAnnouncements; + } + public void setScheduledAnnouncements(List scheduledAnnouncements) { + this.scheduledAnnouncements = scheduledAnnouncements; + } +} diff --git a/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsPlugin.java b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsPlugin.java new file mode 100644 index 000000000..f3a0e4af2 --- /dev/null +++ b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/AnnouncementsPlugin.java @@ -0,0 +1,134 @@ +package net.civmc.announcements; + +import com.cronutils.model.Cron; +import com.cronutils.model.CronType; +import com.cronutils.model.definition.CronDefinitionBuilder; +import com.cronutils.model.time.ExecutionTime; +import com.cronutils.parser.CronParser; +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.plugin.Plugin; +import com.velocitypowered.api.plugin.annotation.DataDirectory; +import com.velocitypowered.api.proxy.ProxyServer; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.ZonedDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; +import org.slf4j.Logger; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +@Plugin(id = "civannouncements", name = "Civ Announcements", version = "1.0.0", + url = "https://civmc.net", description = "Sends various announcements", authors = {"Huskydog9988"}) +public class AnnouncementsPlugin { + + private final CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX)); + + private final ProxyServer server; + private final Logger logger; + private final Path dataDirectory; + + private final Map scheduledAnnouncements = new ConcurrentHashMap<>(); + private AnnouncementsConfig config; + + + @Inject + public AnnouncementsPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory) { + this.server = server; + this.logger = logger; + this.dataDirectory = dataDirectory; + + logger.info("Initializing Announcements plugin"); + + loadConfig(); + scheduleTasks(); + } + + private void scheduleTasks() { + var serializer = JSONComponentSerializer.json(); + + // ensure config exists + if (config == null) { + logger.info("Config is null"); + return; + } + + for (AnnouncementsConfig.ScheduledAnnouncement item : config.getScheduledAnnouncements()) { + Cron cron = parser.parse(item.getCron()); + // convert json message to Component + var formatedMsg = serializer.deserialize(item.getMessage()); + + scheduledAnnouncements.put(cron, formatedMsg); + } + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + // when server is initialized, register listeners + server.getEventManager().register(this, new ProxyListener()); + + // task to check if a scheduled announcement should be sent + server.getScheduler().buildTask(this, this::sendScheduledMessages).repeat(1, TimeUnit.SECONDS).schedule(); + } + + /** + * Checks if a scheduled message needs to be sent, and sends them if so + */ + private void sendScheduledMessages() { + ZonedDateTime now = ZonedDateTime.now(); + + for (Cron cron : scheduledAnnouncements.keySet()) { + var executionTime = ExecutionTime.forCron(cron); + + // if now is a time the + if (executionTime.isMatch(now)) { + server.sendMessage(scheduledAnnouncements.get(cron)); + } + } + } + + /** + * Loads the config from disk, and creates it if necessary + */ + private void loadConfig() { + try { + if (!Files.exists(dataDirectory)) { + Files.createDirectories(dataDirectory); + } + } catch (IOException e) { + logger.error("Could not create data directory: {}", dataDirectory, e); + return; + } + + Path configFile = dataDirectory.resolve("config.yml"); + if (!Files.exists(configFile)) { + try (InputStream in = getClass().getResourceAsStream("/config.yml")) { + if (in != null) { + Files.copy(in, configFile); + logger.info("Default configuration file created."); + } else { + logger.error("Default configuration file is missing in resources!"); + return; + } + } catch (IOException e) { + logger.error("Could not create default configuration file: {}", configFile, e); + } + } + + try (InputStream in = Files.newInputStream(configFile)) { + // register config class with snakeyaml so it knows how to populate the config + Yaml yaml = new Yaml(new Constructor(AnnouncementsConfig.class, new LoaderOptions())); + config = yaml.load(in); + } catch (IOException e) { + logger.error("Could not read config file: {}", configFile, e); + } + } +} diff --git a/plugins/announcements-velocity/src/main/java/net/civmc/announcements/ProxyListener.java b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/ProxyListener.java new file mode 100644 index 000000000..24df42fb9 --- /dev/null +++ b/plugins/announcements-velocity/src/main/java/net/civmc/announcements/ProxyListener.java @@ -0,0 +1,5 @@ +package net.civmc.announcements; + +public class ProxyListener { + +} diff --git a/plugins/announcements-velocity/src/main/resources/config.yml b/plugins/announcements-velocity/src/main/resources/config.yml new file mode 100644 index 000000000..72385ca81 --- /dev/null +++ b/plugins/announcements-velocity/src/main/resources/config.yml @@ -0,0 +1,10 @@ + +# messages must be in json format +# https://minecraft.tools/en/json_text.php +scheduledAnnouncements: + - cron: "45 1 * * *" + message: "{\"text\":\"Restart in 15 minutes\",\"color\":\"gold\"}" + - cron: "55 1 * * *" + message: "{\"text\":\"Restart in 5 minutes\",\"color\":\"gold\"}" + - cron: "59 1 * * *" + message: "{\"text\":\"Restart in 1 minute\",\"color\":\"gold\"}" diff --git a/settings.gradle.kts b/settings.gradle.kts index 9985b1d82..febb93fee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,7 @@ plugins { include(":ansible") +include(":plugins:announcements-velocity") include(":plugins:banstick-paper") include(":plugins:bastion-paper") include(":plugins:castlegates-paper")