diff --git a/build.gradle.kts b/build.gradle.kts index f8e2e0f..56473d6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,10 @@ dependencies { modCompileOnly("dev.isxander", "yet-another-config-lib", property("deps.yacl").toString()) modCompileOnly("com.terraformersmc", "modmenu", property("deps.modmenu").toString()) + + // Discord IPC + include(implementation("com.github.jagrosh:DiscordIPC:master-SNAPSHOT")!!) + include(implementation("org.json:json:20250517")!!) } // Add placeholder-api dependency if property exists diff --git a/src/main/java/cc/aabss/eventutils/DiscordRPC.java b/src/main/java/cc/aabss/eventutils/DiscordRPC.java new file mode 100644 index 0000000..fa1feb8 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/DiscordRPC.java @@ -0,0 +1,135 @@ +package cc.aabss.eventutils; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.server.integrated.IntegratedServer; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.OffsetDateTime; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.jagrosh.discordipc.IPCClient; +import com.jagrosh.discordipc.IPCListener; +import com.jagrosh.discordipc.entities.RichPresence; +import com.jagrosh.discordipc.entities.pipe.PipeStatus; +import org.json.JSONObject; + + +public class DiscordRPC { + private static final OffsetDateTime START = OffsetDateTime.now(); + + @NotNull private final EventUtils mod; + @Nullable private ScheduledFuture presenceTask; + @Nullable public IPCClient client; + + public DiscordRPC(@NotNull EventUtils mod) { + this.mod = mod; + } + + public void connect() { + if (!mod.config.discordRpc) return; + if (client != null && client.getStatus() == PipeStatus.CONNECTED) return; + + client = new IPCClient(1351016544779374735L); + client.setListener(new IPCListener() { + @Override + public void onReady(IPCClient client) { + EventUtils.LOGGER.info("DISCORD RPC: Connected"); + scheduleUpdates(); + } + + @Override + public void onClose(IPCClient client, JSONObject json) { + EventUtils.LOGGER.info("DISCORD RPC: Disconnected ({})", json); + cancelUpdates(); + } + + @Override + public void onDisconnect(IPCClient client, Throwable t) { + EventUtils.LOGGER.warn("DISCORD RPC: Disconnected due to error", t); + cancelUpdates(); + } + }); + + try { + client.connect(); + } catch (final Exception e) { + EventUtils.LOGGER.error("DISCORD RPC: Failed to connect", e); + client = null; + } + } + + public void disconnect() { + cancelUpdates(); + if (client != null) { + try { + client.close(); + } catch (Exception ignored) {} + } + client = null; + } + + private void scheduleUpdates() { + cancelUpdates(); + presenceTask = mod.scheduler.scheduleAtFixedRate(this::updatePresence, 0, 10, TimeUnit.SECONDS); + } + + private void cancelUpdates() { + if (presenceTask != null) { + presenceTask.cancel(false); + presenceTask = null; + } + } + + private void updatePresence() { + if (!mod.config.discordRpc) return; + if (client == null || client.getStatus() != PipeStatus.CONNECTED) return; + + final Status status = Status.get(); + final String username = MinecraftClient.getInstance().getSession().getUsername(); + final RichPresence.Builder builder = new RichPresence.Builder() + .setState("Currently in " + status.text) + .setDetails("Playing as " + username) + .setStartTimestamp(START) + .setLargeImage("logo", "EventUtils" + (Versions.EU_VERSION != null ? " v" + Versions.EU_VERSION : "")) + .setSmallImage("minecraft", "Minecraft" + (Versions.MC_VERSION != null ? " v" + Versions.MC_VERSION : "")); + + // These dont work ig +// builder.set("Download the mod!", "https://modrinth.com/mod/alerts"); +// builder.setButton2("Join the Discord!", "https://discord.gg/aGDuQcduWZ"); + + try { + client.sendRichPresence(builder.build()); + } catch (final Exception e) { + EventUtils.LOGGER.warn("DISCORD RPC: Failed to update presence", e); + } + } + + private enum Status { + SINGLEPLAYER("Singleplayer"), + MULTIPLAYER("Multiplayer"), + MAIN_MENU("the Main Menu"); + + private final String text; + + Status(String text) { + this.text = text; + } + + @NotNull + private static Status get() { + final MinecraftClient client = MinecraftClient.getInstance(); + final IntegratedServer server = client.getServer(); + if (server != null && server.isRunning()) return SINGLEPLAYER; + final ServerInfo entry = client.getCurrentServerEntry(); + if (entry != null && entry.address != null && !Objects.equals(entry.address, "")) return MULTIPLAYER; + return MAIN_MENU; + } + } +} + + diff --git a/src/main/java/cc/aabss/eventutils/EventUtils.java b/src/main/java/cc/aabss/eventutils/EventUtils.java index 31f560c..72c82a0 100644 --- a/src/main/java/cc/aabss/eventutils/EventUtils.java +++ b/src/main/java/cc/aabss/eventutils/EventUtils.java @@ -56,6 +56,7 @@ public class EventUtils implements ClientModInitializer { @NotNull public final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); @NotNull public final Set webSockets = new HashSet<>(); @NotNull public final UpdateChecker updateChecker = new UpdateChecker(this); + @NotNull public final DiscordRPC discordRPC = new DiscordRPC(this); @NotNull public final Map lastIps = new EnumMap<>(EventType.class); public boolean hidePlayers = false; @@ -73,9 +74,13 @@ public void onInitializeClient() { // Command registration ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> CommandRegister.register(dispatcher)); - // Game closed + // Game started / closed + ClientLifecycleEvents.CLIENT_STARTED.register(client -> { + if (config.discordRpc) discordRPC.connect(); + }); ClientLifecycleEvents.CLIENT_STOPPING.register(client -> { webSockets.forEach(socket -> socket.close("Game closed")); + discordRPC.disconnect(); }); // Update checker @@ -108,6 +113,8 @@ public void onInitializeClient() { } if (client.player != null) client.player.sendMessage(Text.literal("No event has happened recently!").formatted(Formatting.RED), true); } + // Keep Discord presence alive if it was toggled on at runtime + if (config.discordRpc) discordRPC.connect(); }); // Simple queue message diff --git a/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java b/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java index 4848dd2..8150fbb 100644 --- a/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java +++ b/src/main/java/cc/aabss/eventutils/config/ConfigScreen.java @@ -31,6 +31,15 @@ public static Screen getConfigScreen(@Nullable Screen parent) { final YetAnotherConfigLib.Builder builder = YetAnotherConfigLib.createBuilder() .title(translatable("eventutils.config.title")) .category(ConfigCategory.createBuilder().name(translatable("eventutils.config.general")) + .option(Option.createBuilder() + .name(translatable("eventutils.config.discord.title")) + .description(OptionDescription.of(translatable("eventutils.config.discord.description"))) + .binding(EventConfig.Defaults.DISCORD_RPC, () -> config.discordRpc, newValue -> { + config.discordRpc = newValue; + config.setSave("discord_rpc", config.discordRpc); + if (Boolean.TRUE.equals(newValue)) EventUtils.MOD.discordRPC.connect(); else EventUtils.MOD.discordRPC.disconnect(); + }) + .controller(ConfigScreen::getBooleanBuilder).build()) .option(Option.createBuilder() .name(translatable("eventutils.config.teleport.title")) .description(OptionDescription.of(translatable("eventutils.config.teleport.description")))