diff --git a/README.md b/README.md index e6d4bde..330e4a8 100644 --- a/README.md +++ b/README.md @@ -7,34 +7,42 @@ Sync advancements, chat relay for Discord and more. This project is inspired by the [CooperativeAdvancements](https://modrinth.com/mod/cooperative-advancements) mod, my goal with this is to add on the coop experience I really enjoyed from CooperativeAdvancements by giving more features in just one package. +Focusing only on server side for now. + ## Features - Bridges a Discord channel to the Minecraft server chat, allowing for chat between the two. - Send events like advancements, join/leave, death, from the server to Discord. - Sync advancements completion, all players share the same advancement progress. - Discord commands to retrieve information about the server. +- Link items in the chat. ## TODO -- Build system needs some work, merging jars is really slow, shadowing is probably not done right, add sources to the artifacts. +- Maybe use a small database library for storage as it might be useful for other ideas. +- Build system needs some work, shadowing is probably not done right, add sources to the artifacts. - Prevent toast from showing for synced players, at the moment the player that was synced will get a toast notification after completing the already completed advancement. -- Add Discord commands to retrieve general information about the server, TPS, uptime, etc. +- Add Discord commands to retrieve general information about the server, TPS, etc. ## Configuration -At startup, a config folder called `cooptweaks` will be created, it will include the following: +Configuration is located in a folder called `cooptweaks`, it contains the following: - `saves`: Folder containing the advancements reached by the players, files are named by the world seed. - `discord.toml`: Configure or enable/disable the Discord bridge. -The Discord bot requires `MESSAGE_CONTENT` and `GUILD_MEMBERS` intents. +The Discord bot requires the permission to create slash commands and `MESSAGE_CONTENT` and `GUILD_MEMBERS` intents. -## Commands +## Server Commands ### `/cooptweaks advancements ` - `progress`: Shows the advancement progress of the world. -## Slash Commands +### `/link` + +Links the item being held by the player to the chat. + +## Slash Commands (Discord) -- `/status`: Shows information about the server like motd, address, etc. +- `/status`: Shows information about the server like motd, uptime, address, etc. diff --git a/common/src/main/java/com/cooptweaks/Main.java b/common/src/main/java/com/cooptweaks/Main.java index 9218fad..252e886 100644 --- a/common/src/main/java/com/cooptweaks/Main.java +++ b/common/src/main/java/com/cooptweaks/Main.java @@ -1,8 +1,9 @@ package com.cooptweaks; import com.cooptweaks.advancements.Advancements; +import com.cooptweaks.advancements.commands.Progress; +import com.cooptweaks.commands.misc.Link; import com.cooptweaks.discord.Discord; -import com.cooptweaks.events.GrantCriterionCallback; import dev.architectury.event.EventResult; import dev.architectury.event.events.common.*; import net.minecraft.util.math.BlockPos; @@ -16,12 +17,15 @@ public final class Main { public static final Discord DISCORD = new Discord(); public static final Advancements ADVANCEMENTS = new Advancements(); + public static long STARTUP; + public static void init() { Configuration.Verify(); LifecycleEvent.SERVER_BEFORE_START.register(server -> DISCORD.Start()); LifecycleEvent.SERVER_STARTED.register(server -> { + STARTUP = System.currentTimeMillis(); DISCORD.NotifyStarted(server); // Requires the server to be started since the seed won't be available until then. @@ -29,6 +33,8 @@ public static void init() { ADVANCEMENTS.LoadAdvancements(server); }); + LifecycleEvent.SERVER_LEVEL_SAVE.register(world -> DISCORD.CyclePresence(world.getPlayers())); + LifecycleEvent.SERVER_STOPPING.register(server -> DISCORD.Stop()); PlayerEvent.PLAYER_JOIN.register(player -> { @@ -76,7 +82,14 @@ public static void init() { return EventResult.pass(); }); - GrantCriterionCallback.EVENT.register(ADVANCEMENTS::OnCriterion); - CommandRegistrationEvent.EVENT.register(ADVANCEMENTS::RegisterCommands); + PlayerEvent.PLAYER_ADVANCEMENT.register(ADVANCEMENTS::OnCriterion); + + CommandRegistrationEvent.EVENT.register((dispatcher, registryAccess, environment) -> { + // Advancements + new Progress().register(dispatcher, registryAccess, environment); + + // Misc + new Link().register(dispatcher, registryAccess, environment); + }); } } diff --git a/common/src/main/java/com/cooptweaks/advancements/Advancements.java b/common/src/main/java/com/cooptweaks/advancements/Advancements.java index d08c838..ab015db 100644 --- a/common/src/main/java/com/cooptweaks/advancements/Advancements.java +++ b/common/src/main/java/com/cooptweaks/advancements/Advancements.java @@ -3,16 +3,12 @@ import com.cooptweaks.Configuration; import com.cooptweaks.Main; import com.cooptweaks.discord.Discord; -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.context.CommandContext; import discord4j.rest.util.Color; import net.minecraft.advancement.Advancement; +import net.minecraft.advancement.AdvancementCriterion; import net.minecraft.advancement.AdvancementEntry; import net.minecraft.advancement.PlayerAdvancementTracker; -import net.minecraft.command.CommandRegistryAccess; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.command.CommandManager; -import net.minecraft.server.command.ServerCommandSource; import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.text.MutableText; import net.minecraft.text.Text; @@ -26,25 +22,53 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +/** + Manages advancements and criteria syncing. +

+

+ Startup: +

    +
  1. Load all advancements and criteria from the server loaded by mods and the game.
  2. +
  3. Load all completed advancements from the save file, the file is named by the world seed.
  4. +
+ p> + Syncing: +
    +
  1. When an advancement is completed, append the save file with the advancement {@link Identifier}.
  2. +
  3. Send a message to the server chat(disabling the original announceAdvancement broadcast) and Discord channel.
  4. +
  5. When a player joins the server, go though all completed advancements and grant the criteria for each to the player.
  6. +
+ */ public final class Advancements { private static final Discord DISCORD = Main.DISCORD; private static MinecraftServer SERVER; + /** Map of all advancements. Loaded at startup. */ private static final HashMap ALL_ADVANCEMENTS = HashMap.newHashMap(122); - private static final ConcurrentHashMap COMPLETED_ADVANCEMENTS = new ConcurrentHashMap<>(122); + + /** + Map of criteria for each advancement. Loaded at startup. +

+ Maps the advancement {@link AdvancementEntry#id() identifier} to a list of criteria. + */ + private static final HashMap> ALL_CRITERIA = new HashMap<>(122); + + /** + Map of all completed advancements. Loaded from the save file. +

+ Maps the advancement {@link AdvancementEntry#id() identifier} to the {@link AdvancementEntry}. + */ + private static final ConcurrentHashMap COMPLETED_ADVANCEMENTS = new ConcurrentHashMap<>(122); private static FileChannel CURRENT_SEED_FILE; - private static synchronized int appendToSave(String text) throws IOException { + private static synchronized void appendToSave(String text) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(text.getBytes()); - return CURRENT_SEED_FILE.write(buffer); + CURRENT_SEED_FILE.write(buffer); } public void LoadAdvancements(MinecraftServer server) { @@ -58,8 +82,14 @@ public void LoadAdvancements(MinecraftServer server) { Main.LOGGER.info("Loaded {} advancements from the server.", totalAdvancements); } + if (ALL_CRITERIA.isEmpty()) { + Main.LOGGER.error("No criteria loaded from the server."); + } else { + Main.LOGGER.info("Loaded {} criteria from the server.", ALL_CRITERIA.size()); + } + try { - int savedAdvancements = loadSaveAdvancements(server, Configuration.ADVANCEMENTS_SAVE_PATH); + int savedAdvancements = loadSaveAdvancements(server); if (savedAdvancements == 0) { Main.LOGGER.info("No completed advancements data to load. Initialized new save file."); } else { @@ -76,17 +106,23 @@ private static int loadServerAdvancements(MinecraftServer server) { for (AdvancementEntry entry : advancements) { Advancement advancement = entry.value(); - // If the advancement has a display, add it to the list. + // If the advancement has a display, add it to the advancement map. advancement.display().ifPresent(display -> { ALL_ADVANCEMENTS.put(entry.id(), entry); + + // Add all criteria for this advancement to the criteria map. + Map> criteria = advancement.criteria(); + criteria.forEach((key, criterion) -> { + ALL_CRITERIA.computeIfAbsent(entry.id(), id -> new ArrayList<>()).add(key); + }); }); } return ALL_ADVANCEMENTS.size(); } - private static int loadSaveAdvancements(MinecraftServer server, Path saveFolder) throws IOException { - Path save = saveFolder.resolve(String.valueOf(server.getOverworld().getSeed())); + private static int loadSaveAdvancements(MinecraftServer server) throws IOException { + Path save = Configuration.ADVANCEMENTS_SAVE_PATH.resolve(String.valueOf(server.getOverworld().getSeed())); if (!Files.exists(save)) { try { @@ -99,26 +135,19 @@ private static int loadSaveAdvancements(MinecraftServer server, Path saveFolder) } BufferedReader reader = new BufferedReader(new FileReader(save.toString())); - String line = reader.readLine(); + String entryName = reader.readLine(); - while (line != null) { - String[] arr = line.split(","); - if (arr.length < 2) { - Main.LOGGER.warn("Skipping malformed line: '{}'", line.trim()); - continue; - } + while (entryName != null) { + Identifier id = Identifier.of(entryName); - String entryName = arr[0]; - String criterionName = arr[1]; - - AdvancementEntry entry = ALL_ADVANCEMENTS.get(Identifier.of(entryName)); + AdvancementEntry entry = ALL_ADVANCEMENTS.get(id); if (entry != null) { - COMPLETED_ADVANCEMENTS.put(criterionName, entry); + COMPLETED_ADVANCEMENTS.put(id, entry); } else { - Main.LOGGER.warn("Advancement '{}' not found for criterion '{}'.", entryName, criterionName); + Main.LOGGER.error("Advancement '{}' not found.", entryName); } - line = reader.readLine(); + entryName = reader.readLine(); } reader.close(); @@ -133,35 +162,46 @@ public void SyncPlayerOnJoin(ServerPlayerEntity player, String name) { PlayerAdvancementTracker tracker = player.getAdvancementTracker(); - Main.LOGGER.info("Syncing {} advancements.", name); - COMPLETED_ADVANCEMENTS.forEach((criterionName, entry) -> tracker.grantCriterion(entry, criterionName)); + Main.LOGGER.info("Syncing player '{}' advancements.", name); + + // Loop through all completed advancements. + COMPLETED_ADVANCEMENTS.forEach((id, entry) -> { + // Loop through all criteria for this advancement. + ALL_CRITERIA.get(id).forEach(criterionName -> { + // Grant the criterion to the player. + tracker.grantCriterion(entry, criterionName); + }); + }); } - public void OnCriterion(ServerPlayerEntity currentPlayer, AdvancementEntry entry, String criterionName, boolean isDone) { - if (COMPLETED_ADVANCEMENTS.containsKey(criterionName)) { + public void OnCriterion(ServerPlayerEntity currentPlayer, AdvancementEntry entry) { + Identifier id = entry.id(); + + if (COMPLETED_ADVANCEMENTS.containsKey(id)) { return; } Advancement advancement = entry.value(); advancement.display().ifPresent(display -> { - if (isDone) { - String id = entry.id().toString(); + PlayerAdvancementTracker tracker = currentPlayer.getAdvancementTracker(); + if (tracker.getProgress(entry).isDone()) { String playerName = currentPlayer.getName().getString(); - COMPLETED_ADVANCEMENTS.put(criterionName, entry); + COMPLETED_ADVANCEMENTS.put(id, entry); + + Collection> criteria = advancement.criteria().values(); // Grant the advancement to all players. List players = SERVER.getPlayerManager().getPlayerList(); for (ServerPlayerEntity player : players) { if (currentPlayer != player) { - player.getAdvancementTracker().grantCriterion(entry, criterionName); + criteria.forEach(criterion -> player.getAdvancementTracker().grantCriterion(entry, criterion.toString())); } } try { - String line = String.format("%s,%s%n", id, criterionName); - int n = appendToSave(line); - Main.LOGGER.info("Saved line '{}' ({} bytes written).", line, n); + String line = String.format("%s%n", id); + appendToSave(line); } catch (IOException e) { // This should be handled in another way, has to be recoverable, so we can try again. throw new RuntimeException(e); @@ -169,7 +209,7 @@ public void OnCriterion(ServerPlayerEntity currentPlayer, AdvancementEntry entry Optional advancementName = advancement.name(); if (advancementName.isEmpty()) { - advancementName = Optional.of(Text.literal(criterionName)); + advancementName = Optional.of(Text.literal(id.toString())); } // Send announcement to the server chat. @@ -181,7 +221,7 @@ public void OnCriterion(ServerPlayerEntity currentPlayer, AdvancementEntry entry // Send announcement to the Discord channel. String title = display.getTitle().getString(); if (title.isEmpty()) { - title = criterionName; + title = id.toString(); } String description = display.getDescription().getString(); @@ -191,14 +231,6 @@ public void OnCriterion(ServerPlayerEntity currentPlayer, AdvancementEntry entry }); } - public void RegisterCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { - dispatcher.register(CommandManager.literal("cooptweaks") - .then(CommandManager.literal("advancements") - .then(CommandManager.literal("progress").executes(this::progressCommand)) - ) - ); - } - /** Gets the advancements progress of the server. @@ -207,9 +239,4 @@ public void RegisterCommands(CommandDispatcher dispatcher, public static String getAdvancementsProgress() { return String.format("%d/%d", COMPLETED_ADVANCEMENTS.size(), ALL_ADVANCEMENTS.size()); } - - private int progressCommand(CommandContext context) { - context.getSource().sendFeedback(() -> Text.literal(String.format("%s advancements completed so far.", getAdvancementsProgress())), false); - return 1; - } } diff --git a/common/src/main/java/com/cooptweaks/advancements/commands/Progress.java b/common/src/main/java/com/cooptweaks/advancements/commands/Progress.java new file mode 100644 index 0000000..eac0689 --- /dev/null +++ b/common/src/main/java/com/cooptweaks/advancements/commands/Progress.java @@ -0,0 +1,27 @@ +package com.cooptweaks.advancements.commands; + +import com.cooptweaks.advancements.Advancements; +import com.cooptweaks.commands.ServerCommand; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.Text; + +public class Progress implements ServerCommand { + @Override + public void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher + .register(CommandManager.literal("cooptweaks") + .then(CommandManager.literal("advancements") + .then(CommandManager.literal("progress") + .executes(this::execute)))); + } + + @Override + public int execute(CommandContext context) { + context.getSource().sendFeedback(() -> Text.literal(String.format("%s advancements completed.", Advancements.getAdvancementsProgress())), false); + return 0; + } +} diff --git a/common/src/main/java/com/cooptweaks/commands/ServerCommand.java b/common/src/main/java/com/cooptweaks/commands/ServerCommand.java new file mode 100644 index 0000000..7e4cde2 --- /dev/null +++ b/common/src/main/java/com/cooptweaks/commands/ServerCommand.java @@ -0,0 +1,16 @@ +package com.cooptweaks.commands; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; + +/* A Minecraft server command. */ +public interface ServerCommand { + /** Registers the command. Some commands are not registered in the "cooptweaks" group. */ + void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment); + + /** Executes the command. */ + int execute(CommandContext context); +} diff --git a/common/src/main/java/com/cooptweaks/commands/misc/Link.java b/common/src/main/java/com/cooptweaks/commands/misc/Link.java new file mode 100644 index 0000000..41d7c6e --- /dev/null +++ b/common/src/main/java/com/cooptweaks/commands/misc/Link.java @@ -0,0 +1,45 @@ +package com.cooptweaks.commands.misc; + +import com.cooptweaks.commands.ServerCommand; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.item.ItemStack; +import net.minecraft.server.command.CommandManager; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; + +public class Link implements ServerCommand { + @Override + public void register(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess, CommandManager.RegistrationEnvironment environment) { + dispatcher + .register(CommandManager.literal("link") + .executes(this::execute)); + } + + @Override + public int execute(CommandContext context) { + ServerCommandSource source = context.getSource(); + ServerPlayerEntity player = source.getPlayer(); + if (player == null) { + return 1; + } + + ItemStack item = player.getMainHandStack(); + + if (item.isEmpty()) { + context.getSource().sendFeedback(() -> Text.of("You are not holding anything."), false); + return 1; + } + + MutableText text = Text.empty(); + text.append(player.getDisplayName()); + text.append(Text.literal(" linked ")); + text.append(item.toHoverableText()); + + source.getServer().getPlayerManager().broadcast(text, false); + return 0; + } +} diff --git a/common/src/main/java/com/cooptweaks/discord/Discord.java b/common/src/main/java/com/cooptweaks/discord/Discord.java index d6fe089..6b13446 100644 --- a/common/src/main/java/com/cooptweaks/discord/Discord.java +++ b/common/src/main/java/com/cooptweaks/discord/Discord.java @@ -6,7 +6,9 @@ import com.cooptweaks.discord.commands.Status; import com.cooptweaks.types.ConfigMap; import com.cooptweaks.types.Result; +import com.cooptweaks.utils.TimeSince; import com.mojang.datafixers.util.Pair; +import discord4j.common.retry.ReconnectOptions; import discord4j.common.store.Store; import discord4j.common.store.impl.LocalStoreLayout; import discord4j.common.util.Snowflake; @@ -19,17 +21,14 @@ import discord4j.core.object.entity.Member; import discord4j.core.object.entity.Message; import discord4j.core.object.entity.User; -import discord4j.core.object.entity.channel.Channel; +import discord4j.core.object.entity.channel.MessageChannel; import discord4j.core.object.presence.ClientActivity; import discord4j.core.object.presence.ClientPresence; import discord4j.core.spec.EmbedCreateSpec; import discord4j.discordjson.json.ApplicationCommandRequest; -import discord4j.discordjson.json.EmbedData; -import discord4j.discordjson.json.ImmutableEmbedData; import discord4j.gateway.intent.Intent; import discord4j.gateway.intent.IntentSet; import discord4j.rest.RestClient; -import discord4j.rest.entity.RestChannel; import discord4j.rest.util.Color; import net.minecraft.server.MinecraftServer; import net.minecraft.server.network.ServerPlayerEntity; @@ -43,6 +42,11 @@ import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; +/** + Handles the Discord bot and its interactions with the server. +

+ The bot is responsible for sending messages to the Discord channel and the server chat. + */ public final class Discord { /** Whether the bot has finished setting up all necessary components. */ private static final AtomicBoolean BOT_READY = new AtomicBoolean(false); @@ -53,7 +57,10 @@ public final class Discord { private static Snowflake BOT_USER_ID; private static Snowflake CHANNEL_ID; - private static RestChannel CHANNEL; + private static MessageChannel CHANNEL; + + private static long LAST_PRESENCE_UPDATE = 0; + private static boolean PRESENCE_CYCLE = true; /** Slash commands. */ private static final HashMap COMMANDS = new HashMap<>(Map.of( @@ -100,6 +107,7 @@ public void Start() { .setStore(Store.fromLayout(LocalStoreLayout.create())) .setEnabledIntents(IntentSet.of(Intent.GUILD_MESSAGES, Intent.MESSAGE_CONTENT, Intent.GUILD_MEMBERS)) .setInitialPresence(presence -> ClientPresence.idle(ClientActivity.watching("the server start"))) + .setReconnectOptions(ReconnectOptions.builder().setMaxRetries(5).setJitterFactor(0.5).build()) .login() .doOnNext(gateway -> { RestClient rest = gateway.getRestClient(); @@ -117,9 +125,7 @@ public void Start() { gateway.on(ChatInputInteractionEvent.class).subscribe(this::onInteraction); }) .flatMap(gateway -> gateway.getChannelById(Snowflake.of(channelId)) - .filter(Objects::nonNull) - .map(Channel::getRestChannel) - .filter(Objects::nonNull) + .ofType(MessageChannel.class) .doOnNext(channel -> { CHANNEL = channel; CHANNEL_ID = channel.getId(); @@ -155,8 +161,6 @@ private void onInteraction(ChatInputInteractionEvent event) { String cmd = event.getCommandName(); if (COMMANDS.containsKey(cmd)) { - Main.LOGGER.info("Executing command '{}'", cmd); - SlashCommand command = COMMANDS.get(cmd); Result embed = command.execute(SERVER); @@ -199,7 +203,7 @@ private void onMessage(MessageCreateEvent event) { } message.getAuthorAsMember() - .filter(Objects::nonNull) + .ofType(Member.class) .flatMap(member -> member.getColor().map(color -> Pair.of(color, member))) .doOnNext(pair -> { Color color = pair.getFirst(); @@ -210,7 +214,7 @@ private void onMessage(MessageCreateEvent event) { // The entire message that will be sent to the server. MutableText text = Text.empty(); - // Appending the GuildMember display name with the same color as the member's highest role. + // Appending the Member display name with the same color as their highest role. text.append(Text.literal(member.getDisplayName()).styled(style -> style.withColor(color.getRGB()))); text.append(Text.literal(" >> " + content)); @@ -251,8 +255,8 @@ public void SendEmbed(String message, Color color) { return; } - ImmutableEmbedData embed = EmbedData.builder() - .color(color.getRGB()) + EmbedCreateSpec embed = EmbedCreateSpec.builder() + .color(color) .description(message) .build(); @@ -261,11 +265,42 @@ public void SendEmbed(String message, Color color) { public void NotifyStarted(MinecraftServer server) { SERVER = server; + LAST_PRESENCE_UPDATE = System.currentTimeMillis(); QueueEvent(() -> SendEmbed("Server started!", Color.GREEN)); } + public void CyclePresence(List players) { + if (!BOT_READY.get()) { + return; + } + + // Only update presence every 5 minutes. + if (System.currentTimeMillis() - LAST_PRESENCE_UPDATE < 5 * 60 * 1000) { + return; + } + + LAST_PRESENCE_UPDATE = System.currentTimeMillis(); + + if (PRESENCE_CYCLE) { + String time = new TimeSince(Main.STARTUP).toString(); + if (time.contains(" and ")) { + time = time.substring(0, time.indexOf(" and ")); + } + + GATEWAY.updatePresence(ClientPresence.online(ClientActivity.playing(String.format("for %s", time)))).subscribe(); + } else { + if (players.isEmpty()) { + GATEWAY.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe(); + } else { + GATEWAY.updatePresence(ClientPresence.online(ClientActivity.watching(String.format("%d players", players.size())))).subscribe(); + } + } + + PRESENCE_CYCLE = !PRESENCE_CYCLE; + } + public void PlayerJoined(String name) { - // In the case on an integrated server, this event might not be called, so queue the event instead. + // In the case on an integrated server, this event might not be called, so queue it instead. QueueEvent(() -> SendEmbed(String.format("**%s** joined!", name), Color.GREEN)); } diff --git a/common/src/main/java/com/cooptweaks/discord/SlashCommand.java b/common/src/main/java/com/cooptweaks/discord/SlashCommand.java index f70d442..5401ebc 100644 --- a/common/src/main/java/com/cooptweaks/discord/SlashCommand.java +++ b/common/src/main/java/com/cooptweaks/discord/SlashCommand.java @@ -5,6 +5,7 @@ import discord4j.discordjson.json.ApplicationCommandRequest; import net.minecraft.server.MinecraftServer; +/** A Discord slash command. */ public interface SlashCommand { /** The name of the command. */ String getName(); diff --git a/common/src/main/java/com/cooptweaks/discord/commands/Status.java b/common/src/main/java/com/cooptweaks/discord/commands/Status.java index 1e48110..a0e3b68 100644 --- a/common/src/main/java/com/cooptweaks/discord/commands/Status.java +++ b/common/src/main/java/com/cooptweaks/discord/commands/Status.java @@ -1,8 +1,10 @@ package com.cooptweaks.discord.commands; +import com.cooptweaks.Main; import com.cooptweaks.advancements.Advancements; import com.cooptweaks.discord.SlashCommand; import com.cooptweaks.types.Result; +import com.cooptweaks.utils.TimeSince; import discord4j.core.spec.EmbedCreateSpec; import discord4j.discordjson.json.ApplicationCommandRequest; import discord4j.rest.util.Color; @@ -38,14 +40,18 @@ public Result execute(MinecraftServer server) { String version = server.getVersion(); String players = String.format("%d/%d", server.getCurrentPlayerCount(), server.getMaxPlayerCount()); String advancements = Advancements.getAdvancementsProgress(); - - String message = String.format("`MOTD`: %s%n`Address`: %s%n`Version`: %s%n`Players`: %s%n`Advancements`: %s", - motd, address, version, players, advancements); + String uptime = new TimeSince(Main.STARTUP).toString(); return Result.success(EmbedCreateSpec.builder() - .color(Color.BLUE) + .color(Color.DEEP_LILAC) .title("Server Status") - .description(message) + .description(motd) + .addField("Address", address, true) + .addField("Version", version, true) + .addField("", "", false) + .addField("Players", players, true) + .addField("Advancements", advancements, true) + .addField("Uptime", uptime, false) .build()); } } diff --git a/common/src/main/java/com/cooptweaks/events/GrantCriterionCallback.java b/common/src/main/java/com/cooptweaks/events/GrantCriterionCallback.java deleted file mode 100644 index 139cfce..0000000 --- a/common/src/main/java/com/cooptweaks/events/GrantCriterionCallback.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.cooptweaks.events; - -import dev.architectury.event.Event; -import dev.architectury.event.EventFactory; -import net.minecraft.advancement.AdvancementEntry; -import net.minecraft.server.network.ServerPlayerEntity; - -public interface GrantCriterionCallback { - Event EVENT = EventFactory.createLoop(GrantCriterionCallback.class); - - void grantCriterion(ServerPlayerEntity player, AdvancementEntry entry, String criterionName, boolean isDone); -} diff --git a/common/src/main/java/com/cooptweaks/mixins/PlayerAdvancementTrackerMixin.java b/common/src/main/java/com/cooptweaks/mixins/PlayerAdvancementTrackerMixin.java index ba19e23..a813d6c 100644 --- a/common/src/main/java/com/cooptweaks/mixins/PlayerAdvancementTrackerMixin.java +++ b/common/src/main/java/com/cooptweaks/mixins/PlayerAdvancementTrackerMixin.java @@ -1,30 +1,12 @@ package com.cooptweaks.mixins; -import com.cooptweaks.events.GrantCriterionCallback; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import net.minecraft.advancement.AdvancementEntry; -import net.minecraft.advancement.AdvancementProgress; import net.minecraft.advancement.PlayerAdvancementTracker; -import net.minecraft.server.network.ServerPlayerEntity; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(PlayerAdvancementTracker.class) public abstract class PlayerAdvancementTrackerMixin { - @Shadow - private ServerPlayerEntity owner; - - @Shadow - public abstract AdvancementProgress getProgress(AdvancementEntry advancement); - - @Inject(method = "grantCriterion", at = @At(value = "RETURN")) - public void grantCriterion(AdvancementEntry entry, String criterionName, CallbackInfoReturnable cir) { - GrantCriterionCallback.EVENT.invoker().grantCriterion(owner, entry, criterionName, getProgress(entry).isDone()); - } - @ModifyExpressionValue(method = "method_53637(Lnet/minecraft/advancement/AdvancementEntry;Lnet/minecraft/advancement/AdvancementDisplay;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/advancement/AdvancementDisplay;shouldAnnounceToChat()Z")) public boolean disableVanillaAnnounceAdvancements(boolean original) { return false; diff --git a/common/src/main/java/com/cooptweaks/utils/TimeSince.java b/common/src/main/java/com/cooptweaks/utils/TimeSince.java new file mode 100644 index 0000000..860c7d5 --- /dev/null +++ b/common/src/main/java/com/cooptweaks/utils/TimeSince.java @@ -0,0 +1,60 @@ +package com.cooptweaks.utils; + +public class TimeSince { + private final long past; + + public TimeSince(long past) { + this.past = past; + } + + /** + Returns a formatted string of time. + + @return "2 days and 16 hours" + */ + public String toString() { + long current = System.currentTimeMillis(); + long elapsed = current - past; + + long seconds = elapsed / 1000; + long minutes = seconds / 60; + long hours = minutes / 60; + long days = hours / 24; + + seconds %= 60; + minutes %= 60; + hours %= 24; + + StringBuilder time = new StringBuilder(); + + if (days > 0) { + time.append(days).append(" day").append(days > 1 ? "s" : ""); + } + + if (hours > 0) { + if (!time.isEmpty()) { + time.append(days > 0 && (minutes > 0 || seconds > 0) ? ", " : " and "); + } + + time.append(hours).append(" hour").append(hours > 1 ? "s" : ""); + } + + if (minutes > 0) { + if (!time.isEmpty()) { + time.append((days > 0 || hours > 0) && seconds > 0 ? ", " : " and "); + } + + time.append(minutes).append(" minute").append(minutes > 1 ? "s" : ""); + } + + if (seconds > 0 || time.isEmpty()) { + if (!time.isEmpty()) { + time.append(" and "); + } + + time.append(seconds).append(" second").append(seconds > 1 ? "s" : ""); + } + + return time.toString(); + } +}