From 179bac291103c7e2ea36a0c52b2162b00dc23cc6 Mon Sep 17 00:00:00 2001 From: "Claude (Chronicler #83 - The Compiler)" Date: Sun, 12 Apr 2026 11:56:15 -0500 Subject: [PATCH] =?UTF-8?q?feat(rules-mod):=20Add=20Firefrost=20Rules=20Mo?= =?UTF-8?q?d=20source=20=E2=80=94=20all=203=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NeoForge 1.21.1, Forge 1.20.1, Forge 1.16.5 All compiled and deployed to NextCloud (Task #136) Source committed for Task #138 (CurseForge generic fork) CLAUDE.md with build environment docs included Claude (Chronicler #83 - The Compiler) --- services/rules-mod/1.16.5/build.gradle | 45 ++++++++++++++++ services/rules-mod/1.16.5/gradle.properties | 2 + services/rules-mod/1.16.5/settings.gradle | 7 +++ .../rules/CooldownManager.java | 31 +++++++++++ .../firefrostgaming/rules/DiscordFetcher.java | 52 +++++++++++++++++++ .../rules/DiscordFormatter.java | 45 ++++++++++++++++ .../com/firefrostgaming/rules/RulesCache.java | 26 ++++++++++ .../firefrostgaming/rules/RulesCommand.java | 44 ++++++++++++++++ .../firefrostgaming/rules/ServerRules.java | 34 ++++++++++++ .../rules/ServerRulesConfig.java | 32 ++++++++++++ .../src/main/resources/META-INF/mods.toml | 26 ++++++++++ services/rules-mod/1.20.1/build.gradle | 50 ++++++++++++++++++ services/rules-mod/1.20.1/gradle.properties | 2 + services/rules-mod/1.20.1/settings.gradle | 8 +++ .../rules/CooldownManager.java | 31 +++++++++++ .../firefrostgaming/rules/DiscordFetcher.java | 45 ++++++++++++++++ .../rules/DiscordFormatter.java | 42 +++++++++++++++ .../com/firefrostgaming/rules/RulesCache.java | 26 ++++++++++ .../firefrostgaming/rules/RulesCommand.java | 44 ++++++++++++++++ .../firefrostgaming/rules/ServerRules.java | 40 ++++++++++++++ .../rules/ServerRulesConfig.java | 32 ++++++++++++ .../src/main/resources/META-INF/mods.toml | 28 ++++++++++ services/rules-mod/1.21.1/build.gradle | 35 +++++++++++++ services/rules-mod/1.21.1/gradle.properties | 10 ++++ services/rules-mod/1.21.1/settings.gradle | 12 +++++ .../rules/CooldownManager.java | 31 +++++++++++ .../firefrostgaming/rules/DiscordFetcher.java | 45 ++++++++++++++++ .../rules/DiscordFormatter.java | 42 +++++++++++++++ .../com/firefrostgaming/rules/RulesCache.java | 26 ++++++++++ .../firefrostgaming/rules/RulesCommand.java | 44 ++++++++++++++++ .../firefrostgaming/rules/ServerRules.java | 41 +++++++++++++++ .../rules/ServerRulesConfig.java | 32 ++++++++++++ .../resources/META-INF/neoforge.mods.toml | 30 +++++++++++ services/rules-mod/CLAUDE.md | 23 ++++++++ 34 files changed, 1063 insertions(+) create mode 100644 services/rules-mod/1.16.5/build.gradle create mode 100644 services/rules-mod/1.16.5/gradle.properties create mode 100644 services/rules-mod/1.16.5/settings.gradle create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/CooldownManager.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCache.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCommand.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRules.java create mode 100644 services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java create mode 100644 services/rules-mod/1.16.5/src/main/resources/META-INF/mods.toml create mode 100755 services/rules-mod/1.20.1/build.gradle create mode 100755 services/rules-mod/1.20.1/gradle.properties create mode 100755 services/rules-mod/1.20.1/settings.gradle create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCache.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRules.java create mode 100755 services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java create mode 100755 services/rules-mod/1.20.1/src/main/resources/META-INF/mods.toml create mode 100644 services/rules-mod/1.21.1/build.gradle create mode 100644 services/rules-mod/1.21.1/gradle.properties create mode 100644 services/rules-mod/1.21.1/settings.gradle create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCache.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRules.java create mode 100644 services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java create mode 100644 services/rules-mod/1.21.1/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 services/rules-mod/CLAUDE.md diff --git a/services/rules-mod/1.16.5/build.gradle b/services/rules-mod/1.16.5/build.gradle new file mode 100644 index 0000000..fb42ff2 --- /dev/null +++ b/services/rules-mod/1.16.5/build.gradle @@ -0,0 +1,45 @@ +buildscript { + repositories { + maven { url = 'https://maven.minecraftforge.net/' } + mavenCentral() + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+' + } +} + +apply plugin: 'net.minecraftforge.gradle' + +version = '1.0.0' +group = 'com.firefrostgaming.rules' +archivesBaseName = 'firefrostrules' + +java.toolchain.languageVersion = JavaLanguageVersion.of(8) + +minecraft { + mappings channel: 'official', version: '1.16.5' + runs { + server { + workingDirectory project.file('run') + mods { + firefrostrules { + source sourceSets.main + } + } + } + } +} + +dependencies { + minecraft 'net.minecraftforge:forge:1.16.5-36.2.39' +} + +jar { + manifest { + attributes(["Implementation-Title": "Firefrost Rules", "Implementation-Version": project.version]) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} diff --git a/services/rules-mod/1.16.5/gradle.properties b/services/rules-mod/1.16.5/gradle.properties new file mode 100644 index 0000000..29b2ac4 --- /dev/null +++ b/services/rules-mod/1.16.5/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false diff --git a/services/rules-mod/1.16.5/settings.gradle b/services/rules-mod/1.16.5/settings.gradle new file mode 100644 index 0000000..187f5e4 --- /dev/null +++ b/services/rules-mod/1.16.5/settings.gradle @@ -0,0 +1,7 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.minecraftforge.net/' } + } +} +rootProject.name = 'firefrostrules' diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/CooldownManager.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/CooldownManager.java new file mode 100644 index 0000000..589087e --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/CooldownManager.java @@ -0,0 +1,31 @@ +package com.firefrostgaming.rules; + +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.entity.player.ServerPlayerEntity; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class CooldownManager { + private static final ConcurrentHashMap COOLDOWNS = new ConcurrentHashMap<>(); + + public static boolean checkAndUpdateCooldown(ServerPlayerEntity player) { + UUID playerId = player.getUUID(); + Instant now = Instant.now(); + int cooldownSeconds = ServerRulesConfig.COOLDOWN_SECONDS.get(); + Instant lastUsed = COOLDOWNS.get(playerId); + if (lastUsed != null) { + long secondsSinceLastUse = Duration.between(lastUsed, now).getSeconds(); + if (secondsSinceLastUse < cooldownSeconds) { + long remaining = cooldownSeconds - secondsSinceLastUse; + player.sendMessage(new StringTextComponent("\u00A7cPlease wait " + remaining + " seconds before checking the rules again."), player.getUUID()); + return false; + } + } + COOLDOWNS.put(playerId, now); + return true; + } + + public static void removePlayer(UUID playerId) { COOLDOWNS.remove(playerId); } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java new file mode 100644 index 0000000..8d4ce04 --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java @@ -0,0 +1,52 @@ +package com.firefrostgaming.rules; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.CompletableFuture; + +public class DiscordFetcher { + private static final Logger LOGGER = LogManager.getLogger(DiscordFetcher.class); + + public static CompletableFuture fetchRulesAsync() { + if (!ServerRulesConfig.isMessageIdValid()) { + LOGGER.error("Invalid Discord Message ID in config."); + return CompletableFuture.completedFuture(null); + } + return CompletableFuture.supplyAsync(() -> { + try { + String token = ServerRulesConfig.BOT_TOKEN.get(); + String channelId = ServerRulesConfig.CHANNEL_ID.get(); + String messageId = ServerRulesConfig.MESSAGE_ID.get(); + URL url = new URL("https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("GET"); + con.setRequestProperty("Authorization", "Bot " + token); + con.setRequestProperty("Accept", "application/json"); + con.setConnectTimeout(10000); + con.setReadTimeout(10000); + int status = con.getResponseCode(); + if (status == 200) { + BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); + StringBuilder content = new StringBuilder(); + String inputLine; + while ((inputLine = in.readLine()) != null) { content.append(inputLine); } + in.close(); + JsonObject json = new JsonParser().parse(content.toString()).getAsJsonObject(); + return json.get("content").getAsString(); + } else { + LOGGER.error("Discord API returned status: {}", status); + return null; + } + } catch (Exception ex) { + LOGGER.error("Network error while fetching Discord rules", ex); + return null; + } + }); + } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java new file mode 100644 index 0000000..d7a71ac --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java @@ -0,0 +1,45 @@ +package com.firefrostgaming.rules; + +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.TextFormatting; +import net.minecraft.util.text.IFormattableTextComponent; + +public class DiscordFormatter { + public static IFormattableTextComponent formatRules(String rawDiscordText) { + String processedText = convertEmojis(rawDiscordText); + String lowerText = processedText.toLowerCase(); + TextFormatting headerColor = TextFormatting.DARK_PURPLE; + TextFormatting bodyColor = TextFormatting.LIGHT_PURPLE; + if (lowerText.contains("fire") || lowerText.contains("[fire]")) { + headerColor = TextFormatting.GOLD; + bodyColor = TextFormatting.YELLOW; + } else if (lowerText.contains("frost") || lowerText.contains("[frost]")) { + headerColor = TextFormatting.AQUA; + bodyColor = TextFormatting.DARK_AQUA; + } + IFormattableTextComponent rootComponent = new StringTextComponent(""); + String[] lines = processedText.split("\n"); + for (String line : lines) { + IFormattableTextComponent lineComponent; + if (line.startsWith("**") && line.endsWith("**")) { + String cleanLine = line.replace("**", ""); + lineComponent = new StringTextComponent(cleanLine); + lineComponent.withStyle(headerColor, TextFormatting.BOLD); + } else if (line.trim().startsWith("-") || line.trim().startsWith("\u2022")) { + lineComponent = new StringTextComponent(" " + line.trim()); + lineComponent.withStyle(bodyColor); + } else { + lineComponent = new StringTextComponent(line); + lineComponent.withStyle(bodyColor); + } + rootComponent.append(lineComponent).append(new StringTextComponent("\n")); + } + return rootComponent; + } + + private static String convertEmojis(String text) { + if (text == null) return ""; + return text.replace("\uD83D\uDD25", "[Fire]").replace("\u2744\uFE0F", "[Frost]") + .replace("\uD83D\uDC9C", "[Arcane]"); + } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCache.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCache.java new file mode 100644 index 0000000..c4f557d --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCache.java @@ -0,0 +1,26 @@ +package com.firefrostgaming.rules; + +import java.time.Instant; + +public class RulesCache { + private static String cachedRules = null; + private static Instant lastFetchTime = Instant.MIN; + private static final String FALLBACK_RULES = + "[Fire] Server Rules\n1. Be respectful to all players.\n2. No griefing or cheating.\n3. Follow staff instructions.\nPlease check Discord for the full rules list."; + + public static boolean isCacheValid() { + if (cachedRules == null) return false; + long cacheMinutes = ServerRulesConfig.CACHE_MINUTES.get(); + return Instant.now().isBefore(lastFetchTime.plusSeconds(cacheMinutes * 60)); + } + + public static void updateCache(String newRules) { + if (newRules != null && !newRules.trim().isEmpty()) { + cachedRules = newRules; + lastFetchTime = Instant.now(); + } + } + + public static String getRules() { return cachedRules != null ? cachedRules : FALLBACK_RULES; } + public static void invalidate() { cachedRules = null; lastFetchTime = Instant.MIN; } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCommand.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCommand.java new file mode 100644 index 0000000..d2d52cb --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/RulesCommand.java @@ -0,0 +1,44 @@ +package com.firefrostgaming.rules; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.command.CommandSource; +import net.minecraft.command.Commands; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.util.text.StringTextComponent; +import net.minecraft.util.text.IFormattableTextComponent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class RulesCommand { + private static final Logger LOGGER = LogManager.getLogger(RulesCommand.class); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("rules").executes(context -> { + CommandSource source = context.getSource(); + if (!(source.getEntity() instanceof ServerPlayerEntity)) { + source.sendSuccess(DiscordFormatter.formatRules(RulesCache.getRules()), false); + return 1; + } + ServerPlayerEntity player = (ServerPlayerEntity) source.getEntity(); + if (!CooldownManager.checkAndUpdateCooldown(player)) return 0; + if (RulesCache.isCacheValid()) { + player.sendMessage(DiscordFormatter.formatRules(RulesCache.getRules()), player.getUUID()); + return 1; + } + player.sendMessage(new StringTextComponent("\u00A77Fetching latest rules..."), player.getUUID()); + DiscordFetcher.fetchRulesAsync().thenAccept(fetchedRules -> { + String rulesText; + if (fetchedRules != null) { + RulesCache.updateCache(fetchedRules); + rulesText = fetchedRules; + } else { + LOGGER.warn("Discord fetch failed. Falling back to cache for {}", player.getName().getString()); + rulesText = RulesCache.getRules(); + } + IFormattableTextComponent formattedRules = DiscordFormatter.formatRules(rulesText); + source.getServer().execute(() -> player.sendMessage(formattedRules, player.getUUID())); + }); + return 1; + })); + } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRules.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRules.java new file mode 100644 index 0000000..7d09960 --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRules.java @@ -0,0 +1,34 @@ +package com.firefrostgaming.rules; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Mod("firefrostrules") +public class ServerRules { + private static final Logger LOGGER = LogManager.getLogger(ServerRules.class); + + public ServerRules() { + ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ServerRulesConfig.SPEC); + MinecraftForge.EVENT_BUS.register(this); + LOGGER.info("Firefrost Rules Mod Initialized."); + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + RulesCommand.register(event.getDispatcher()); + LOGGER.info("Registered /rules command."); + } + + @SubscribeEvent + public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + CooldownManager.removePlayer(event.getEntity().getUUID()); + } +} diff --git a/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java new file mode 100644 index 0000000..a046a3e --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java @@ -0,0 +1,32 @@ +package com.firefrostgaming.rules; + +import net.minecraftforge.common.ForgeConfigSpec; +import org.apache.commons.lang3.StringUtils; + +public class ServerRulesConfig { + public static final ForgeConfigSpec SPEC; + public static final ForgeConfigSpec.ConfigValue BOT_TOKEN; + public static final ForgeConfigSpec.ConfigValue CHANNEL_ID; + public static final ForgeConfigSpec.ConfigValue MESSAGE_ID; + public static final ForgeConfigSpec.IntValue COOLDOWN_SECONDS; + public static final ForgeConfigSpec.IntValue CACHE_MINUTES; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + builder.push("discord"); + BOT_TOKEN = builder.comment("Discord Bot Token").define("bot_token", "YOUR_TOKEN_HERE"); + CHANNEL_ID = builder.comment("Discord Channel ID").define("channel_id", "1234567890123456789"); + MESSAGE_ID = builder.comment("Discord Message ID").define("message_id", "1234567890123456789"); + builder.pop(); + builder.push("performance"); + COOLDOWN_SECONDS = builder.comment("Per-player cooldown in seconds").defineInRange("cooldown_seconds", 60, 0, 3600); + CACHE_MINUTES = builder.comment("Cache duration in minutes").defineInRange("cache_minutes", 30, 1, 1440); + builder.pop(); + SPEC = builder.build(); + } + + public static boolean isMessageIdValid() { + String id = MESSAGE_ID.get(); + return StringUtils.isNotBlank(id) && id.matches("^\\d{17,20}$"); + } +} diff --git a/services/rules-mod/1.16.5/src/main/resources/META-INF/mods.toml b/services/rules-mod/1.16.5/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..8bf47ef --- /dev/null +++ b/services/rules-mod/1.16.5/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader="javafml" +loaderVersion="[36,)" +license="All Rights Reserved" +issueTrackerURL="https://firefrostgaming.com/support" + +[[mods]] +modId="firefrostrules" +version="${file.jarVersion}" +displayName="Firefrost Rules" +displayURL="https://firefrostgaming.com" +authors="Firefrost Gaming" +description='''Fetches server rules dynamically from Discord for the /rules command.''' + +[[dependencies.firefrostrules]] +modId="forge" +mandatory=true +versionRange="[36,)" +ordering="NONE" +side="SERVER" + +[[dependencies.firefrostrules]] +modId="minecraft" +mandatory=true +versionRange="[1.16.5,1.17)" +ordering="NONE" +side="SERVER" diff --git a/services/rules-mod/1.20.1/build.gradle b/services/rules-mod/1.20.1/build.gradle new file mode 100755 index 0000000..b4ab99a --- /dev/null +++ b/services/rules-mod/1.20.1/build.gradle @@ -0,0 +1,50 @@ +buildscript { + repositories { + maven { url = 'https://maven.minecraftforge.net/' } + mavenCentral() + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '6.0.+' + } +} + +apply plugin: 'net.minecraftforge.gradle' + +version = '1.0.0' +group = 'com.firefrostgaming.rules' +archivesBaseName = 'firefrostrules' + +java.toolchain.languageVersion = JavaLanguageVersion.of(17) + +minecraft { + mappings channel: 'official', version: '1.20.1' + runs { + server { + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + mods { + firefrostrules { + source sourceSets.main + } + } + } + } +} + +dependencies { + minecraft 'net.minecraftforge:forge:1.20.1-47.3.0' +} + +jar { + manifest { + attributes([ + "Implementation-Title": "Firefrost Rules", + "Implementation-Version": project.version, + ]) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} diff --git a/services/rules-mod/1.20.1/gradle.properties b/services/rules-mod/1.20.1/gradle.properties new file mode 100755 index 0000000..29b2ac4 --- /dev/null +++ b/services/rules-mod/1.20.1/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false diff --git a/services/rules-mod/1.20.1/settings.gradle b/services/rules-mod/1.20.1/settings.gradle new file mode 100755 index 0000000..7c71559 --- /dev/null +++ b/services/rules-mod/1.20.1/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.minecraftforge.net/' } + } +} + +rootProject.name = 'firefrostrules' diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java new file mode 100755 index 0000000..243e86d --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java @@ -0,0 +1,31 @@ +package com.firefrostgaming.rules; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class CooldownManager { + private static final ConcurrentHashMap COOLDOWNS = new ConcurrentHashMap<>(); + + public static boolean checkAndUpdateCooldown(ServerPlayer player) { + UUID playerId = player.getUUID(); + Instant now = Instant.now(); + int cooldownSeconds = ServerRulesConfig.COOLDOWN_SECONDS.get(); + Instant lastUsed = COOLDOWNS.get(playerId); + if (lastUsed != null) { + long secondsSinceLastUse = Duration.between(lastUsed, now).getSeconds(); + if (secondsSinceLastUse < cooldownSeconds) { + long remaining = cooldownSeconds - secondsSinceLastUse; + player.sendSystemMessage(Component.literal("\u00A7cPlease wait " + remaining + " seconds before checking the rules again.")); + return false; + } + } + COOLDOWNS.put(playerId, now); + return true; + } + + public static void removePlayer(UUID playerId) { COOLDOWNS.remove(playerId); } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java new file mode 100755 index 0000000..20c3269 --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java @@ -0,0 +1,45 @@ +package com.firefrostgaming.rules; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class DiscordFetcher { + private static final Logger LOGGER = LoggerFactory.getLogger(DiscordFetcher.class); + private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + public static CompletableFuture fetchRulesAsync() { + if (!ServerRulesConfig.isMessageIdValid()) { + LOGGER.error("Invalid Discord Message ID in config."); + return CompletableFuture.completedFuture(null); + } + String token = ServerRulesConfig.BOT_TOKEN.get(); + String channelId = ServerRulesConfig.CHANNEL_ID.get(); + String messageId = ServerRulesConfig.MESSAGE_ID.get(); + URI uri = URI.create("https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId); + HttpRequest request = HttpRequest.newBuilder().uri(uri) + .header("Authorization", "Bot " + token) + .header("Accept", "application/json").GET().build(); + return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() == 200) { + JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); + return json.get("content").getAsString(); + } else { + LOGGER.error("Discord API returned status: {}", response.statusCode()); + return null; + } + }) + .exceptionally(ex -> { + LOGGER.error("Network error while fetching Discord rules", ex); + return null; + }); + } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java new file mode 100755 index 0000000..b7296c1 --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java @@ -0,0 +1,42 @@ +package com.firefrostgaming.rules; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class DiscordFormatter { + public static MutableComponent formatRules(String rawDiscordText) { + String processedText = convertEmojis(rawDiscordText); + String lowerText = processedText.toLowerCase(); + ChatFormatting headerColor = ChatFormatting.DARK_PURPLE; + ChatFormatting bodyColor = ChatFormatting.LIGHT_PURPLE; + if (lowerText.contains("fire") || lowerText.contains("[fire]")) { + headerColor = ChatFormatting.GOLD; + bodyColor = ChatFormatting.YELLOW; + } else if (lowerText.contains("frost") || lowerText.contains("[frost]")) { + headerColor = ChatFormatting.AQUA; + bodyColor = ChatFormatting.DARK_AQUA; + } + MutableComponent rootComponent = Component.empty(); + String[] lines = processedText.split("\n"); + for (String line : lines) { + MutableComponent lineComponent; + if (line.startsWith("**") && line.endsWith("**")) { + String cleanLine = line.replace("**", ""); + lineComponent = Component.literal(cleanLine).withStyle(headerColor, ChatFormatting.BOLD); + } else if (line.trim().startsWith("-") || line.trim().startsWith("\u2022")) { + lineComponent = Component.literal(" " + line.trim()).withStyle(bodyColor); + } else { + lineComponent = Component.literal(line).withStyle(bodyColor); + } + rootComponent.append(lineComponent).append(Component.literal("\n")); + } + return rootComponent; + } + + private static String convertEmojis(String text) { + if (text == null) return ""; + return text.replace("\uD83D\uDD25", "[Fire]").replace("\u2744\uFE0F", "[Frost]") + .replace("\uD83D\uDC9C", "[Arcane]").replaceAll("[\\x{1F300}-\\x{1F9FF}]", ""); + } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCache.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCache.java new file mode 100755 index 0000000..c4f557d --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCache.java @@ -0,0 +1,26 @@ +package com.firefrostgaming.rules; + +import java.time.Instant; + +public class RulesCache { + private static String cachedRules = null; + private static Instant lastFetchTime = Instant.MIN; + private static final String FALLBACK_RULES = + "[Fire] Server Rules\n1. Be respectful to all players.\n2. No griefing or cheating.\n3. Follow staff instructions.\nPlease check Discord for the full rules list."; + + public static boolean isCacheValid() { + if (cachedRules == null) return false; + long cacheMinutes = ServerRulesConfig.CACHE_MINUTES.get(); + return Instant.now().isBefore(lastFetchTime.plusSeconds(cacheMinutes * 60)); + } + + public static void updateCache(String newRules) { + if (newRules != null && !newRules.trim().isEmpty()) { + cachedRules = newRules; + lastFetchTime = Instant.now(); + } + } + + public static String getRules() { return cachedRules != null ? cachedRules : FALLBACK_RULES; } + public static void invalidate() { cachedRules = null; lastFetchTime = Instant.MIN; } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java new file mode 100755 index 0000000..8cee128 --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java @@ -0,0 +1,44 @@ +package com.firefrostgaming.rules; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RulesCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(RulesCommand.class); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("rules").executes(context -> { + CommandSourceStack source = context.getSource(); + if (source.getEntity() == null || !(source.getEntity() instanceof ServerPlayer)) { + source.sendSystemMessage(DiscordFormatter.formatRules(RulesCache.getRules())); + return 1; + } + ServerPlayer player = (ServerPlayer) source.getEntity(); + if (!CooldownManager.checkAndUpdateCooldown(player)) return 0; + if (RulesCache.isCacheValid()) { + player.sendSystemMessage(DiscordFormatter.formatRules(RulesCache.getRules())); + return 1; + } + player.sendSystemMessage(Component.literal("\u00A77Fetching latest rules...")); + DiscordFetcher.fetchRulesAsync().thenAccept(fetchedRules -> { + String rulesText; + if (fetchedRules != null) { + RulesCache.updateCache(fetchedRules); + rulesText = fetchedRules; + } else { + LOGGER.warn("Discord fetch failed. Falling back to cached rules for {}", player.getName().getString()); + rulesText = RulesCache.getRules(); + } + MutableComponent formattedRules = DiscordFormatter.formatRules(rulesText); + source.getServer().execute(() -> player.sendSystemMessage(formattedRules)); + }); + return 1; + })); + } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRules.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRules.java new file mode 100755 index 0000000..2d65ce9 --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRules.java @@ -0,0 +1,40 @@ +package com.firefrostgaming.rules; + +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; +import net.minecraftforge.fml.event.config.ModConfigEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Mod("firefrostrules") +public class ServerRules { + private static final Logger LOGGER = LoggerFactory.getLogger(ServerRules.class); + + public ServerRules() { + ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, ServerRulesConfig.SPEC); + MinecraftForge.EVENT_BUS.register(this); + LOGGER.info("Firefrost Rules Mod Initialized."); + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + RulesCommand.register(event.getDispatcher()); + LOGGER.info("Registered /rules command."); + } + + @SubscribeEvent + public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + CooldownManager.removePlayer(event.getEntity().getUUID()); + } + + @SubscribeEvent + public void onConfigReload(ModConfigEvent.Reloading event) { + LOGGER.info("Rules configuration reloaded! Invalidating cache."); + RulesCache.invalidate(); + } +} diff --git a/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java new file mode 100755 index 0000000..a046a3e --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java @@ -0,0 +1,32 @@ +package com.firefrostgaming.rules; + +import net.minecraftforge.common.ForgeConfigSpec; +import org.apache.commons.lang3.StringUtils; + +public class ServerRulesConfig { + public static final ForgeConfigSpec SPEC; + public static final ForgeConfigSpec.ConfigValue BOT_TOKEN; + public static final ForgeConfigSpec.ConfigValue CHANNEL_ID; + public static final ForgeConfigSpec.ConfigValue MESSAGE_ID; + public static final ForgeConfigSpec.IntValue COOLDOWN_SECONDS; + public static final ForgeConfigSpec.IntValue CACHE_MINUTES; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + builder.push("discord"); + BOT_TOKEN = builder.comment("Discord Bot Token").define("bot_token", "YOUR_TOKEN_HERE"); + CHANNEL_ID = builder.comment("Discord Channel ID").define("channel_id", "1234567890123456789"); + MESSAGE_ID = builder.comment("Discord Message ID").define("message_id", "1234567890123456789"); + builder.pop(); + builder.push("performance"); + COOLDOWN_SECONDS = builder.comment("Per-player cooldown in seconds").defineInRange("cooldown_seconds", 60, 0, 3600); + CACHE_MINUTES = builder.comment("Cache duration in minutes").defineInRange("cache_minutes", 30, 1, 1440); + builder.pop(); + SPEC = builder.build(); + } + + public static boolean isMessageIdValid() { + String id = MESSAGE_ID.get(); + return StringUtils.isNotBlank(id) && id.matches("^\\d{17,20}$"); + } +} diff --git a/services/rules-mod/1.20.1/src/main/resources/META-INF/mods.toml b/services/rules-mod/1.20.1/src/main/resources/META-INF/mods.toml new file mode 100755 index 0000000..43cdf24 --- /dev/null +++ b/services/rules-mod/1.20.1/src/main/resources/META-INF/mods.toml @@ -0,0 +1,28 @@ +modLoader="javafml" +loaderVersion="[47,)" +license="All Rights Reserved" +issueTrackerURL="https://firefrostgaming.com/support" + +[[mods]] +modId="firefrostrules" +version="${file.jarVersion}" +displayName="Firefrost Rules" +displayURL="https://firefrostgaming.com" +authors="Firefrost Gaming" +description=''' +Fetches server rules dynamically from Discord for the /rules command. +''' + +[[dependencies.firefrostrules]] +modId="forge" +mandatory=true +versionRange="[47,)" +ordering="NONE" +side="SERVER" + +[[dependencies.firefrostrules]] +modId="minecraft" +mandatory=true +versionRange="[1.20.1,1.21)" +ordering="NONE" +side="SERVER" diff --git a/services/rules-mod/1.21.1/build.gradle b/services/rules-mod/1.21.1/build.gradle new file mode 100644 index 0000000..a0334f7 --- /dev/null +++ b/services/rules-mod/1.21.1/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java-library' + id 'net.neoforged.moddev' version '2.0.141' +} + +version = mod_version +group = mod_group_id + +repositories { + mavenCentral() +} + +dependencies { +} + +neoForge { + version = neo_version + + runs { + server { + server() + } + } + + mods { + "${mod_id}" { + sourceSet sourceSets.main + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release.set(21) +} diff --git a/services/rules-mod/1.21.1/gradle.properties b/services/rules-mod/1.21.1/gradle.properties new file mode 100644 index 0000000..b7d3096 --- /dev/null +++ b/services/rules-mod/1.21.1/gradle.properties @@ -0,0 +1,10 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +minecraft_version=1.21.1 +neo_version=21.1.65 + +mod_id=firefrostrules +mod_name=Firefrost Rules +mod_version=1.0.0 +mod_group_id=com.firefrostgaming.rules diff --git a/services/rules-mod/1.21.1/settings.gradle b/services/rules-mod/1.21.1/settings.gradle new file mode 100644 index 0000000..fe8e4be --- /dev/null +++ b/services/rules-mod/1.21.1/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +rootProject.name = 'firefrostrules' diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java new file mode 100644 index 0000000..243e86d --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/CooldownManager.java @@ -0,0 +1,31 @@ +package com.firefrostgaming.rules; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class CooldownManager { + private static final ConcurrentHashMap COOLDOWNS = new ConcurrentHashMap<>(); + + public static boolean checkAndUpdateCooldown(ServerPlayer player) { + UUID playerId = player.getUUID(); + Instant now = Instant.now(); + int cooldownSeconds = ServerRulesConfig.COOLDOWN_SECONDS.get(); + Instant lastUsed = COOLDOWNS.get(playerId); + if (lastUsed != null) { + long secondsSinceLastUse = Duration.between(lastUsed, now).getSeconds(); + if (secondsSinceLastUse < cooldownSeconds) { + long remaining = cooldownSeconds - secondsSinceLastUse; + player.sendSystemMessage(Component.literal("\u00A7cPlease wait " + remaining + " seconds before checking the rules again.")); + return false; + } + } + COOLDOWNS.put(playerId, now); + return true; + } + + public static void removePlayer(UUID playerId) { COOLDOWNS.remove(playerId); } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java new file mode 100644 index 0000000..20c3269 --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFetcher.java @@ -0,0 +1,45 @@ +package com.firefrostgaming.rules; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +public class DiscordFetcher { + private static final Logger LOGGER = LoggerFactory.getLogger(DiscordFetcher.class); + private static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + + public static CompletableFuture fetchRulesAsync() { + if (!ServerRulesConfig.isMessageIdValid()) { + LOGGER.error("Invalid Discord Message ID in config."); + return CompletableFuture.completedFuture(null); + } + String token = ServerRulesConfig.BOT_TOKEN.get(); + String channelId = ServerRulesConfig.CHANNEL_ID.get(); + String messageId = ServerRulesConfig.MESSAGE_ID.get(); + URI uri = URI.create("https://discord.com/api/v10/channels/" + channelId + "/messages/" + messageId); + HttpRequest request = HttpRequest.newBuilder().uri(uri) + .header("Authorization", "Bot " + token) + .header("Accept", "application/json").GET().build(); + return CLIENT.sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .thenApply(response -> { + if (response.statusCode() == 200) { + JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); + return json.get("content").getAsString(); + } else { + LOGGER.error("Discord API returned status: {}", response.statusCode()); + return null; + } + }) + .exceptionally(ex -> { + LOGGER.error("Network error while fetching Discord rules", ex); + return null; + }); + } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java new file mode 100644 index 0000000..b7296c1 --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/DiscordFormatter.java @@ -0,0 +1,42 @@ +package com.firefrostgaming.rules; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class DiscordFormatter { + public static MutableComponent formatRules(String rawDiscordText) { + String processedText = convertEmojis(rawDiscordText); + String lowerText = processedText.toLowerCase(); + ChatFormatting headerColor = ChatFormatting.DARK_PURPLE; + ChatFormatting bodyColor = ChatFormatting.LIGHT_PURPLE; + if (lowerText.contains("fire") || lowerText.contains("[fire]")) { + headerColor = ChatFormatting.GOLD; + bodyColor = ChatFormatting.YELLOW; + } else if (lowerText.contains("frost") || lowerText.contains("[frost]")) { + headerColor = ChatFormatting.AQUA; + bodyColor = ChatFormatting.DARK_AQUA; + } + MutableComponent rootComponent = Component.empty(); + String[] lines = processedText.split("\n"); + for (String line : lines) { + MutableComponent lineComponent; + if (line.startsWith("**") && line.endsWith("**")) { + String cleanLine = line.replace("**", ""); + lineComponent = Component.literal(cleanLine).withStyle(headerColor, ChatFormatting.BOLD); + } else if (line.trim().startsWith("-") || line.trim().startsWith("\u2022")) { + lineComponent = Component.literal(" " + line.trim()).withStyle(bodyColor); + } else { + lineComponent = Component.literal(line).withStyle(bodyColor); + } + rootComponent.append(lineComponent).append(Component.literal("\n")); + } + return rootComponent; + } + + private static String convertEmojis(String text) { + if (text == null) return ""; + return text.replace("\uD83D\uDD25", "[Fire]").replace("\u2744\uFE0F", "[Frost]") + .replace("\uD83D\uDC9C", "[Arcane]").replaceAll("[\\x{1F300}-\\x{1F9FF}]", ""); + } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCache.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCache.java new file mode 100644 index 0000000..c4f557d --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCache.java @@ -0,0 +1,26 @@ +package com.firefrostgaming.rules; + +import java.time.Instant; + +public class RulesCache { + private static String cachedRules = null; + private static Instant lastFetchTime = Instant.MIN; + private static final String FALLBACK_RULES = + "[Fire] Server Rules\n1. Be respectful to all players.\n2. No griefing or cheating.\n3. Follow staff instructions.\nPlease check Discord for the full rules list."; + + public static boolean isCacheValid() { + if (cachedRules == null) return false; + long cacheMinutes = ServerRulesConfig.CACHE_MINUTES.get(); + return Instant.now().isBefore(lastFetchTime.plusSeconds(cacheMinutes * 60)); + } + + public static void updateCache(String newRules) { + if (newRules != null && !newRules.trim().isEmpty()) { + cachedRules = newRules; + lastFetchTime = Instant.now(); + } + } + + public static String getRules() { return cachedRules != null ? cachedRules : FALLBACK_RULES; } + public static void invalidate() { cachedRules = null; lastFetchTime = Instant.MIN; } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java new file mode 100644 index 0000000..11d3b9d --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/RulesCommand.java @@ -0,0 +1,44 @@ +package com.firefrostgaming.rules; + +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RulesCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(RulesCommand.class); + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("rules").executes(context -> { + CommandSourceStack source = context.getSource(); + if (!source.isPlayer()) { + source.sendSystemMessage(DiscordFormatter.formatRules(RulesCache.getRules())); + return 1; + } + ServerPlayer player = source.getPlayer(); + if (!CooldownManager.checkAndUpdateCooldown(player)) return 0; + if (RulesCache.isCacheValid()) { + player.sendSystemMessage(DiscordFormatter.formatRules(RulesCache.getRules())); + return 1; + } + player.sendSystemMessage(Component.literal("\u00A77Fetching latest rules...")); + DiscordFetcher.fetchRulesAsync().thenAccept(fetchedRules -> { + String rulesText; + if (fetchedRules != null) { + RulesCache.updateCache(fetchedRules); + rulesText = fetchedRules; + } else { + LOGGER.warn("Discord fetch failed. Falling back to cached rules for {}", player.getName().getString()); + rulesText = RulesCache.getRules(); + } + MutableComponent formattedRules = DiscordFormatter.formatRules(rulesText); + source.getServer().execute(() -> player.sendSystemMessage(formattedRules)); + }); + return 1; + })); + } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRules.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRules.java new file mode 100644 index 0000000..a2e143b --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRules.java @@ -0,0 +1,41 @@ +package com.firefrostgaming.rules; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.fml.event.config.ModConfigEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Mod("firefrostrules") +public class ServerRules { + private static final Logger LOGGER = LoggerFactory.getLogger(ServerRules.class); + + public ServerRules(IEventBus modEventBus, ModContainer modContainer) { + modContainer.registerConfig(ModConfig.Type.SERVER, ServerRulesConfig.SPEC); + modEventBus.addListener(this::onConfigReload); + NeoForge.EVENT_BUS.register(this); + LOGGER.info("Firefrost Rules Mod Initialized."); + } + + @SubscribeEvent + public void onRegisterCommands(RegisterCommandsEvent event) { + RulesCommand.register(event.getDispatcher()); + LOGGER.info("Registered /rules command."); + } + + @SubscribeEvent + public void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) { + CooldownManager.removePlayer(event.getEntity().getUUID()); + } + + private void onConfigReload(ModConfigEvent.Reloading event) { + LOGGER.info("Rules configuration reloaded! Invalidating cache."); + RulesCache.invalidate(); + } +} diff --git a/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java new file mode 100644 index 0000000..c7883e9 --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/java/com/firefrostgaming/rules/ServerRulesConfig.java @@ -0,0 +1,32 @@ +package com.firefrostgaming.rules; + +import net.neoforged.neoforge.common.ModConfigSpec; +import org.apache.commons.lang3.StringUtils; + +public class ServerRulesConfig { + public static final ModConfigSpec SPEC; + public static final ModConfigSpec.ConfigValue BOT_TOKEN; + public static final ModConfigSpec.ConfigValue CHANNEL_ID; + public static final ModConfigSpec.ConfigValue MESSAGE_ID; + public static final ModConfigSpec.IntValue COOLDOWN_SECONDS; + public static final ModConfigSpec.IntValue CACHE_MINUTES; + + static { + ModConfigSpec.Builder builder = new ModConfigSpec.Builder(); + builder.push("discord"); + BOT_TOKEN = builder.comment("Discord Bot Token").define("bot_token", "YOUR_TOKEN_HERE"); + CHANNEL_ID = builder.comment("Discord Channel ID").define("channel_id", "1234567890123456789"); + MESSAGE_ID = builder.comment("Discord Message ID").define("message_id", "1234567890123456789"); + builder.pop(); + builder.push("performance"); + COOLDOWN_SECONDS = builder.comment("Per-player cooldown in seconds").defineInRange("cooldown_seconds", 60, 0, 3600); + CACHE_MINUTES = builder.comment("Cache duration in minutes").defineInRange("cache_minutes", 30, 1, 1440); + builder.pop(); + SPEC = builder.build(); + } + + public static boolean isMessageIdValid() { + String id = MESSAGE_ID.get(); + return StringUtils.isNotBlank(id) && id.matches("^\\d{17,20}$"); + } +} diff --git a/services/rules-mod/1.21.1/src/main/resources/META-INF/neoforge.mods.toml b/services/rules-mod/1.21.1/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..ea660d7 --- /dev/null +++ b/services/rules-mod/1.21.1/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,30 @@ +modLoader="javafml" +loaderVersion="[4,)" +license="All Rights Reserved" +issueTrackerURL="https://firefrostgaming.com/support" +showAsResourcePack=false + +[[mods]] +modId="firefrostrules" +version="${file.jarVersion}" +displayName="Firefrost Rules" +displayURL="https://firefrostgaming.com" +authors="Firefrost Gaming" +description=''' +Fetches server rules dynamically from Discord for the /rules command. +Built for long-term stability and easy community management. +''' + +[[dependencies.firefrostrules]] +modId="neoforge" +type="required" +versionRange="[21.1,)" +ordering="NONE" +side="SERVER" + +[[dependencies.firefrostrules]] +modId="minecraft" +type="required" +versionRange="[1.21.1,1.22)" +ordering="NONE" +side="SERVER" diff --git a/services/rules-mod/CLAUDE.md b/services/rules-mod/CLAUDE.md new file mode 100644 index 0000000..016a1df --- /dev/null +++ b/services/rules-mod/CLAUDE.md @@ -0,0 +1,23 @@ +# Firefrost Rules Mod — Build Environment + +## Project Structure +- `1.21.1/` — NeoForge (Java 21, Gradle 8.8, moddev 2.0.141) +- `1.20.1/` — Forge (Java 17, Gradle 8.8, ForgeGradle 6.0) +- `1.16.5/` — Forge (Java 8, Gradle 7.6.4, ForgeGradle 5.1) + +## What This Mod Does +Player types `/rules` → mod fetches rules from a Discord message → displays in-game with colored formatting. Admins update rules by editing a Discord message — no restarts, no file editing. + +## 7 Source Files (each version) +- ServerRules.java — main mod class +- ServerRulesConfig.java — TOML config (bot token, channel ID, message ID) +- RulesCommand.java — /rules command handler +- DiscordFetcher.java — async HTTP fetch from Discord API +- DiscordFormatter.java — Discord markdown → Minecraft chat formatting +- RulesCache.java — 30-minute cache with fallback +- CooldownManager.java — per-player 60-second cooldown + +## Version Differences +- 1.21.1: `net.neoforged.*`, `ModConfigSpec`, `Component.literal()`, `java.net.http.HttpClient` +- 1.20.1: `net.minecraftforge.*`, `ForgeConfigSpec`, `Component.literal()`, `java.net.http.HttpClient` +- 1.16.5: `net.minecraftforge.*`, `ForgeConfigSpec`, `StringTextComponent`, `HttpURLConnection`, `new JsonParser().parse()`