From ca4fa41e181748c676492032ebea6b862d1dfcb8 Mon Sep 17 00:00:00 2001 From: Aries Powvalla Date: Sat, 9 Aug 2025 21:32:45 -0700 Subject: [PATCH 1/5] DA: add nearby events to server list --- API_REFERENCE.md | 110 ++++++++ build.gradle.kts | 8 +- gradlew | 0 .../aabss/eventutils/EventServerManager.java | 258 ++++++++++++++++++ .../java/cc/aabss/eventutils/EventUtils.java | 50 ++++ .../eventutils/commands/TeleportCmd.java | 2 + .../mixin/MultiplayerScreenMixin.java | 24 ++ .../eventutils/websocket/SocketEndpoint.java | 16 ++ src/main/resources/eventutils.mixin.json | 1 + 9 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 API_REFERENCE.md mode change 100644 => 100755 gradlew create mode 100644 src/main/java/cc/aabss/eventutils/EventServerManager.java create mode 100644 src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java diff --git a/API_REFERENCE.md b/API_REFERENCE.md new file mode 100644 index 0000000..1f7051e --- /dev/null +++ b/API_REFERENCE.md @@ -0,0 +1,110 @@ +# Type Examples +- **String:** `hello` +- **Integer:** `2023` +- **Long:** `242385234992037888` +- **Array:** `element1,element2,element3` +- **Map:** `{key1:"value1",key2:"value2",key3:"value3"}` +# Database/static +**API Prefix:** https://eventalerts.gg/api/v1/ENDPOINT +## Comparisons +- `equals`: The document's value must be the same as the given one +- `contains`: The document's value must contain all of the given ones +## Misc +- `unique`: Only 1 document can have this value, so only 1 will be returned, always (unless something goes terribly wrong) +## Endpoints +- `servers` + - **Example:** https://eventalerts.gg/api/v1/servers?representatives=242385234992037888,365630764244664320&name=test + Will return servers with the name `test` that have srnyx *and* Oiiink as representatives + - **Query Parameters** + - `id`, string, equals, unique + - `message`, long, equals, unique + - `name`, string, equals + - `description`, string, equals + - `invite`, string, equals + - `created`, long, equals + - `tags`, string array, contains + - `color`, integer, equals + - `thumbnail`, string, equals + - `gets`, string array, contains key + - `representatives`, long array, contains +- `players` + - **Example:** https://eventalerts.gg/api/v1/players?boosterPasses=314853603695394817 + Will return any user that has given Skeppy a Booster Pass (should just be one, as you can only get a Booster Pass from one person at a time) + - **Query Parameters** + - `id`, string, equals, unique + - `user`, long, equals, unique + - `anniversaries`, integer array, contains + - `boosterPasses`, long array, contains +- `events` + - **Example:** https://eventalerts.gg/api/v1/events?channel=980956946075115570 + Will get all active events in the Partner events channel + - **Query Parameters** + - `id`, string, equals, unique + - `thread`, long, equals, unique + - `channel`, long, equals + - `title`, string, equals + - `time`, long, equals + - `subscribers`, long array, contains +# Websockets +**API Prefix:** `wss://eventalerts.venox.network/api/v1/socket/SOCKET` +*You must use `wss://`, not `ws://`, as our sockets are encrypted!* +## Conditions +- `builder`: Only included if the event was posted using `/event builder` +## Endpoints +- `server_enabled` + - **Keys** + - `id`, string + - `representatives`, long array + - `created`, long + - `name`, string + - `description`, string + - `invite`, string + - `tags`, string array + - `color`, integer + - `thumbnail`, string + - `message`, long + - `gets`, [string, string] map +- `server_edited` + - **Keys** + - `id`, string + - `representatives`, long array + - `created`, long + - `name`, string + - `description`, string + - `invite`, string + - `tags`, string array + - `color`, integer + - `thumbnail`, string + - `message`, long + - `gets`, [string, string] map +- `booster_pass_given` + - **Keys** + - `id`, string + - `representatives`, long array + - `created`, long + - `name`, string + - `description`, string + - `invite`, string + - `tags`, string array + - `color`, integer + - `thumbnail`, string + - `message`, long + - `gets`, [string, string] map +- `event_posted` + - **Keys** + - `id`, string + - `user`, long + - `anniversaries`, integer array + - `booster_passes`, long array +- `event_cancelled` + - **Keys** + - `channel`, long + - `thread`, long + - `id`, string + - `title`, string + - `time`, long, builder + - `subscribers`, long array, builder +- `potential_famous_event` + - This will just be a string of the message that contained the role ping +- `famous_event` + - This will just be a string of the message that contained the role ping \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index f8e2e0f..7493279 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,11 +25,11 @@ dependencies { minecraft("com.mojang", "minecraft", property("deps.minecraft").toString()) mappings("net.fabricmc", "yarn", property("deps.yarn_mappings").toString()) - modCompileOnly("net.fabricmc", "fabric-loader", property("deps.fabric_loader").toString()) - modCompileOnly("net.fabricmc.fabric-api", "fabric-api", property("deps.fabric_api").toString()) + modImplementation("net.fabricmc", "fabric-loader", property("deps.fabric_loader").toString()) + modImplementation("net.fabricmc.fabric-api", "fabric-api", property("deps.fabric_api").toString()) - modCompileOnly("dev.isxander", "yet-another-config-lib", property("deps.yacl").toString()) - modCompileOnly("com.terraformersmc", "modmenu", property("deps.modmenu").toString()) + modImplementation("dev.isxander", "yet-another-config-lib", property("deps.yacl").toString()) + modImplementation("com.terraformersmc", "modmenu", property("deps.modmenu").toString()) } // Add placeholder-api dependency if property exists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/cc/aabss/eventutils/EventServerManager.java b/src/main/java/cc/aabss/eventutils/EventServerManager.java new file mode 100644 index 0000000..368ad42 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/EventServerManager.java @@ -0,0 +1,258 @@ +package cc.aabss.eventutils; + +import cc.aabss.eventutils.utility.ConnectUtility; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.client.option.ServerList; + +import com.google.gson.JsonObject; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class EventServerManager { + private static final String EVENT_SERVER_PREFIX = "§7[Event] §r"; + private static final String EVENTS_DIVIDER_NAME = "§8--- Events ---"; + + @NotNull private final EventUtils mod; + @NotNull private final Map activeEventServers = new HashMap<>(); + @NotNull private final Map> removalTasks = new HashMap<>(); + @Nullable private ServerList serverList; + + public EventServerManager(@NotNull EventUtils mod) { + this.mod = mod; + } + + public void setServerList(@Nullable ServerList serverList) { + this.serverList = serverList; + } + + public void addEventServer(@NotNull JsonObject eventJson) { + final MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) return; + + // Extract event information + final String eventId = eventJson.has("id") ? eventJson.get("id").getAsString() : ("event-" + System.currentTimeMillis()); + final String title = eventJson.has("title") ? eventJson.get("title").getAsString() : "Event"; + final long eventTime = eventJson.has("time") ? eventJson.get("time").getAsLong() : 0; + + // Try to extract server IP from various possible fields + String serverIp = extractServerIp(eventJson); + if (serverIp == null || serverIp.isEmpty()) { + EventUtils.LOGGER.warn("No server IP found for event: {}", title); + return; + } + + // Don't add if already exists (fast-path check) + if (activeEventServers.containsKey(eventId)) return; + + client.execute(() -> { + if (!ensureServerListLoaded()) { + EventUtils.LOGGER.warn("Server list not available, cannot add event server"); + return; + } + + // Create server info + final String serverName = EVENT_SERVER_PREFIX + title; + final ServerInfo serverInfo = new ServerInfo(serverName, serverIp, ServerInfo.ServerType.OTHER); + serverInfo.setResourcePackPolicy(ServerInfo.ResourcePackPolicy.PROMPT); + + // Add events divider if it doesn't exist + addEventsDividerIfNeeded(serverList); + + // Add the server to the list (avoid duplicates in the persistent list) + for (int i = 0; i < serverList.size(); i++) { + final ServerInfo existing = serverList.get(i); + if (existing.name.equals(serverName) && existing.address.equalsIgnoreCase(serverIp)) { + EventUtils.LOGGER.info("Event server already present in server list: '{}' -> '{}'", serverName, serverIp); + return; + } + } + serverList.add(serverInfo, false); + + // Store event server info + final EventServerInfo eventServerInfo = new EventServerInfo(eventId, serverInfo, eventTime); + activeEventServers.put(eventId, eventServerInfo); + + // Schedule removal when event starts + if (eventTime > 0) { + final long currentTime = System.currentTimeMillis(); + final long timeUntilEvent = eventTime - currentTime; + + if (timeUntilEvent > 0) { + final ScheduledFuture removalTask = mod.scheduler.schedule( + () -> removeEventServer(eventId), + timeUntilEvent, + TimeUnit.MILLISECONDS + ); + removalTasks.put(eventId, removalTask); + EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms", title, timeUntilEvent); + } else { + // Event has already started, don't add it + serverList.remove(serverInfo); + activeEventServers.remove(eventId); + EventUtils.LOGGER.info("Event '{}' has already started, not adding to server list", title); + return; + } + } + + // Persist changes to disk so they show up when user opens the Multiplayer screen later + try { + serverList.saveFile(); + } catch (final Exception e) { + EventUtils.LOGGER.error("Failed to save server list after adding event server", e); + } + + EventUtils.LOGGER.info("Added event server '{}' with IP '{}' to server list", title, serverIp); + }); + } + + public void removeEventServer(@NotNull String eventId) { + final MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) return; + client.execute(() -> { + final EventServerInfo eventServerInfo = activeEventServers.remove(eventId); + if (eventServerInfo == null) return; + + if (!ensureServerListLoaded()) { + EventUtils.LOGGER.warn("Server list not available, cannot remove event server"); + return; + } + + // Remove from server list by matching properties (instance may differ if server list was reloaded) + int removedCount = 0; + for (int i = serverList.size() - 1; i >= 0; i--) { + final ServerInfo candidate = serverList.get(i); + if (candidate.name.equals(eventServerInfo.serverInfo.name) + && candidate.address.equalsIgnoreCase(eventServerInfo.serverInfo.address)) { + serverList.remove(candidate); + removedCount++; + } + } + if (removedCount == 0) { + EventUtils.LOGGER.warn("Event server not found in current server list for removal: '{}' -> '{}'", eventServerInfo.serverInfo.name, eventServerInfo.serverInfo.address); + } + + // Cancel removal task + final ScheduledFuture removalTask = removalTasks.remove(eventId); + if (removalTask != null) { + removalTask.cancel(false); + } + + // Remove events divider if no more event servers + if (activeEventServers.isEmpty()) { + removeEventsDivider(serverList); + } + + // Persist removal + try { + serverList.saveFile(); + } catch (final Exception e) { + EventUtils.LOGGER.error("Failed to save server list after removing event server", e); + } + + EventUtils.LOGGER.info("Removed event server from server list: {}", eventServerInfo.serverInfo.name); + }); + } + + public void removeAllEventServers() { + // Cancel all removal tasks + removalTasks.values().forEach(task -> task.cancel(false)); + removalTasks.clear(); + + // Remove all event servers + for (final String eventId : new HashMap<>(activeEventServers).keySet()) { + removeEventServer(eventId); + } + } + + @Nullable + private String extractServerIp(@NotNull JsonObject eventJson) { + // Try to get IP from common fields in the event JSON + if (eventJson.has("ip")) { + return eventJson.get("ip").getAsString(); + } + if (eventJson.has("server")) { + return eventJson.get("server").getAsString(); + } + if (eventJson.has("address")) { + return eventJson.get("address").getAsString(); + } + + // Try to extract from description using the existing utility + if (eventJson.has("description")) { + final String description = eventJson.get("description").getAsString(); + final String extractedIp = ConnectUtility.getIp(description); + if (extractedIp != null && !extractedIp.isEmpty()) { + return extractedIp; + } + } + + // Try to extract from title as well + if (eventJson.has("title")) { + final String title = eventJson.get("title").getAsString(); + final String extractedIp = ConnectUtility.getIp(title); + if (extractedIp != null && !extractedIp.isEmpty()) { + return extractedIp; + } + } + + // No IP could be determined + return null; + } + + private void addEventsDividerIfNeeded(@NotNull ServerList serverList) { + // Check if divider already exists + for (int i = 0; i < serverList.size(); i++) { + final ServerInfo server = serverList.get(i); + if (server.name.equals(EVENTS_DIVIDER_NAME)) { + return; // Divider already exists + } + } + + // Add divider at the end + final ServerInfo divider = new ServerInfo(EVENTS_DIVIDER_NAME, "", ServerInfo.ServerType.OTHER); + serverList.add(divider, false); + } + + private void removeEventsDivider(@NotNull ServerList serverList) { + for (int i = 0; i < serverList.size(); i++) { + final ServerInfo server = serverList.get(i); + if (server.name.equals(EVENTS_DIVIDER_NAME)) { + serverList.remove(server); + return; + } + } + } + + private boolean ensureServerListLoaded() { + if (this.serverList != null) return true; + final MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) return false; + this.serverList = new ServerList(client); + try { + this.serverList.loadFile(); + } catch (final Exception e) { + EventUtils.LOGGER.error("Failed to load server list", e); + } + return true; + } + + private static class EventServerInfo { + @NotNull public final String eventId; + @NotNull public final ServerInfo serverInfo; + public final long eventTime; + + public EventServerInfo(@NotNull String eventId, @NotNull ServerInfo serverInfo, long eventTime) { + this.eventId = eventId; + this.serverInfo = serverInfo; + this.eventTime = eventTime; + } + } +} diff --git a/src/main/java/cc/aabss/eventutils/EventUtils.java b/src/main/java/cc/aabss/eventutils/EventUtils.java index 31f560c..2de3804 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 EventServerManager eventServerManager = new EventServerManager(this); @NotNull public final Map lastIps = new EnumMap<>(EventType.class); public boolean hidePlayers = false; @@ -69,6 +70,7 @@ public void onInitializeClient() { // Websockets webSockets.add(new WebSocketClient(this, SocketEndpoint.EVENT_POSTED)); webSockets.add(new WebSocketClient(this, SocketEndpoint.FAMOUS_EVENT_POSTED)); + webSockets.add(new WebSocketClient(this, SocketEndpoint.EVENT_CANCELLED)); // Command registration ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> CommandRegister.register(dispatcher)); @@ -76,6 +78,7 @@ public void onInitializeClient() { // Game closed ClientLifecycleEvents.CLIENT_STOPPING.register(client -> { webSockets.forEach(socket -> socket.close("Game closed")); + eventServerManager.removeAllEventServers(); }); // Update checker @@ -92,6 +95,11 @@ public void onInitializeClient() { InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, "key.category.eventutils")); + final KeyBinding testEventKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "key.eventutils.testevent", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_SEMICOLON, + "key.category.eventutils")); ClientTickEvents.END_CLIENT_TICK.register(client -> { // Hide players key if (hidePlayersKey.wasPressed()) { @@ -108,6 +116,17 @@ public void onInitializeClient() { } if (client.player != null) client.player.sendMessage(Text.literal("No event has happened recently!").formatted(Formatting.RED), true); } + + // Test event key + if (testEventKey.wasPressed()) { + simulateTestEvent(); + if (client.player != null) { + client.player.sendMessage(Text.literal("Test event simulated! Check your server list and you should see a toast notification.").formatted(Formatting.GREEN), true); + } else { + // In main menu, just log it + LOGGER.info("Test event simulated from main menu"); + } + } }); // Simple queue message @@ -177,4 +196,35 @@ public static int max(int... values) { public static String translate(@NotNull String key) { return Language.getInstance().get(key); } + + /** + * Simulates an event being posted for testing purposes. + * Creates a test event that starts in 5 minutes. + */ + public void simulateTestEvent() { + final long currentTime = System.currentTimeMillis(); + final long eventTime = currentTime + (1 * 30 * 1000); + + // Create a test event JSON object with the correct structure + final JsonObject testEvent = new JsonObject(); + testEvent.addProperty("id", "test-event-" + currentTime); + testEvent.addProperty("title", "Test Event"); + testEvent.addProperty("description", "This is a simulated test event for testing the server list feature. Server: mc.hypixel.net"); + testEvent.addProperty("time", eventTime); + testEvent.addProperty("ip", "invadedlands.net"); + testEvent.addProperty("prize", "$1000"); + + // Add the rolesNamed array that EventType.fromJson expects + final com.google.gson.JsonArray rolesArray = new com.google.gson.JsonArray(); + rolesArray.add("MONEY"); + testEvent.add("rolesNamed", rolesArray); + + LOGGER.info("Simulating test event: {}", testEvent.toString()); + + // Process the event through the EVENT_POSTED handler + SocketEndpoint.EVENT_POSTED.handler.accept(this, testEvent.toString()); + + // Set as last event for event info screen + SocketEndpoint.LAST_EVENT = testEvent; + } } diff --git a/src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java b/src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java index 8bfabc0..8ef7a0a 100644 --- a/src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java +++ b/src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java @@ -31,6 +31,8 @@ public static void teleport(@NotNull CommandContext c return; } + System.out.println("Connecting to " + lastIp + " for event " + type.name().toLowerCase()); + // Connect ConnectUtility.connect(lastIp); } diff --git a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java new file mode 100644 index 0000000..807b941 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java @@ -0,0 +1,24 @@ +package cc.aabss.eventutils.mixin; + +import cc.aabss.eventutils.EventUtils; +import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; +import net.minecraft.client.option.ServerList; +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.CallbackInfo; + +@Mixin(MultiplayerScreen.class) +public class MultiplayerScreenMixin { + + @Shadow private ServerList serverList; + + @Inject(method = "init", at = @At("TAIL")) + private void onInit(CallbackInfo ci) { + // Store reference to server list for EventServerManager + if (EventUtils.MOD != null) { + EventUtils.MOD.eventServerManager.setServerList(this.serverList); + } + } +} diff --git a/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java b/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java index 3ac6b60..5fa52e8 100644 --- a/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java +++ b/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java @@ -32,6 +32,11 @@ public enum SocketEndpoint { // Send toast eventType.sendToast(mod, prizeAmount > 0 ? prizeAmount : null, ip != null && !ip.isEmpty()); mod.lastIps.put(eventType, ip); + + // Add event server to server list if it has an IP + if (ip != null && !ip.isEmpty()) { + mod.eventServerManager.addEventServer(json); + } } }), FAMOUS_EVENT_POSTED((mod, message) -> { @@ -50,6 +55,17 @@ public enum SocketEndpoint { String ip = mod.getIpAndConnect(eventType, json); eventType.sendToast(mod, null, ip != null && !ip.isEmpty()); mod.lastIps.put(eventType, mod.getIpAndConnect(eventType, json)); + }), + EVENT_CANCELLED((mod, message) -> { + // Get JSON + final JsonObject json = parseJson(message); + if (json == null) return; + + // Remove event server from server list if it exists + if (json.has("id")) { + final String eventId = json.get("id").getAsString(); + mod.eventServerManager.removeEventServer(eventId); + } }); @Nullable public static JsonObject LAST_EVENT; diff --git a/src/main/resources/eventutils.mixin.json b/src/main/resources/eventutils.mixin.json index ad911e2..2b06b48 100644 --- a/src/main/resources/eventutils.mixin.json +++ b/src/main/resources/eventutils.mixin.json @@ -10,6 +10,7 @@ "ButtonWidgetMixin", "ClientMixin", "EntityRenderDispatcherMixin", + "MultiplayerScreenMixin", "PlayerEntityRendererMixin" ], "injectors": { From e27f9970498d3666b6ce50812ab913a2a9d6c1a5 Mon Sep 17 00:00:00 2001 From: Aries Powvalla Date: Sun, 10 Aug 2025 09:41:25 -0700 Subject: [PATCH 2/5] feat: approaching events on server list --- .../aabss/eventutils/EventServerManager.java | 67 +++++++------------ .../mixin/MultiplayerScreenMixin.java | 31 +++++++++ 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/src/main/java/cc/aabss/eventutils/EventServerManager.java b/src/main/java/cc/aabss/eventutils/EventServerManager.java index 368ad42..cf8207c 100644 --- a/src/main/java/cc/aabss/eventutils/EventServerManager.java +++ b/src/main/java/cc/aabss/eventutils/EventServerManager.java @@ -17,8 +17,7 @@ import java.util.concurrent.TimeUnit; public class EventServerManager { - private static final String EVENT_SERVER_PREFIX = "§7[Event] §r"; - private static final String EVENTS_DIVIDER_NAME = "§8--- Events ---"; + public static final String EVENT_SERVER_PREFIX = "§7[Event] §r"; @NotNull private final EventUtils mod; @NotNull private final Map activeEventServers = new HashMap<>(); @@ -63,9 +62,6 @@ public void addEventServer(@NotNull JsonObject eventJson) { final ServerInfo serverInfo = new ServerInfo(serverName, serverIp, ServerInfo.ServerType.OTHER); serverInfo.setResourcePackPolicy(ServerInfo.ResourcePackPolicy.PROMPT); - // Add events divider if it doesn't exist - addEventsDividerIfNeeded(serverList); - // Add the server to the list (avoid duplicates in the persistent list) for (int i = 0; i < serverList.size(); i++) { final ServerInfo existing = serverList.get(i); @@ -80,25 +76,37 @@ public void addEventServer(@NotNull JsonObject eventJson) { final EventServerInfo eventServerInfo = new EventServerInfo(eventId, serverInfo, eventTime); activeEventServers.put(eventId, eventServerInfo); - // Schedule removal when event starts + // Schedule removal 5 minutes after event starts if (eventTime > 0) { final long currentTime = System.currentTimeMillis(); - final long timeUntilEvent = eventTime - currentTime; + final long graceMs = TimeUnit.MINUTES.toMillis(5); + final long timeUntilRemoval = (eventTime + graceMs) - currentTime; - if (timeUntilEvent > 0) { + if (timeUntilRemoval > 0) { final ScheduledFuture removalTask = mod.scheduler.schedule( () -> removeEventServer(eventId), - timeUntilEvent, + timeUntilRemoval, TimeUnit.MILLISECONDS ); removalTasks.put(eventId, removalTask); - EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms", title, timeUntilEvent); + EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms (5m after start)", title, timeUntilRemoval); } else { - // Event has already started, don't add it - serverList.remove(serverInfo); - activeEventServers.remove(eventId); - EventUtils.LOGGER.info("Event '{}' has already started, not adding to server list", title); - return; + // If within 5-minute grace after event start, keep it briefly; else do not add + if (currentTime - eventTime <= graceMs) { + final long remaining = graceMs - (currentTime - eventTime); + final ScheduledFuture removalTask = mod.scheduler.schedule( + () -> removeEventServer(eventId), + remaining, + TimeUnit.MILLISECONDS + ); + removalTasks.put(eventId, removalTask); + EventUtils.LOGGER.info("Event '{}' already started; keeping for {} ms (grace)", title, remaining); + } else { + serverList.remove(serverInfo); + activeEventServers.remove(eventId); + EventUtils.LOGGER.info("Event '{}' started more than 5 minutes ago; not adding", title); + return; + } } } @@ -145,11 +153,6 @@ public void removeEventServer(@NotNull String eventId) { removalTask.cancel(false); } - // Remove events divider if no more event servers - if (activeEventServers.isEmpty()) { - removeEventsDivider(serverList); - } - // Persist removal try { serverList.saveFile(); @@ -207,28 +210,8 @@ private String extractServerIp(@NotNull JsonObject eventJson) { return null; } - private void addEventsDividerIfNeeded(@NotNull ServerList serverList) { - // Check if divider already exists - for (int i = 0; i < serverList.size(); i++) { - final ServerInfo server = serverList.get(i); - if (server.name.equals(EVENTS_DIVIDER_NAME)) { - return; // Divider already exists - } - } - - // Add divider at the end - final ServerInfo divider = new ServerInfo(EVENTS_DIVIDER_NAME, "", ServerInfo.ServerType.OTHER); - serverList.add(divider, false); - } - - private void removeEventsDivider(@NotNull ServerList serverList) { - for (int i = 0; i < serverList.size(); i++) { - final ServerInfo server = serverList.get(i); - if (server.name.equals(EVENTS_DIVIDER_NAME)) { - serverList.remove(server); - return; - } - } + public int getActiveEventCount() { + return activeEventServers.size(); } private boolean ensureServerListLoaded() { diff --git a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java index 807b941..b3e934f 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java @@ -1,18 +1,23 @@ package cc.aabss.eventutils.mixin; import cc.aabss.eventutils.EventUtils; +import cc.aabss.eventutils.EventServerManager; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.option.ServerList; +import net.minecraft.client.network.ServerInfo; 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.CallbackInfo; +import net.minecraft.client.gui.DrawContext; + @Mixin(MultiplayerScreen.class) public class MultiplayerScreenMixin { @Shadow private ServerList serverList; + @Shadow private net.minecraft.client.gui.screen.multiplayer.MultiplayerServerListWidget serverListWidget; @Inject(method = "init", at = @At("TAIL")) private void onInit(CallbackInfo ci) { @@ -21,4 +26,30 @@ private void onInit(CallbackInfo ci) { EventUtils.MOD.eventServerManager.setServerList(this.serverList); } } + + @Inject(method = "render", at = @At("TAIL")) + private void highlightEventRows(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + if (serverListWidget == null) return; + // Row-by-row highlight for event servers + final int left = serverListWidget.getRowLeft(); + final int right = left + serverListWidget.getRowWidth(); + final int n = serverListWidget.children().size(); + for (int i = 0; i < n; i++) { + final var entry = serverListWidget.children().get(i); + final var narration = entry.getNarration(); + if (narration == null) continue; + final String label = narration.getString(); + final String normalized = label.replaceAll("\u00A7.", ""); + final boolean isEvent = label.contains(EventServerManager.EVENT_SERVER_PREFIX) || normalized.contains("[Event] "); + if (!isEvent) continue; + + final int top = serverListWidget.getRowTop(i); + final int bottom = (i + 1 < n) ? serverListWidget.getRowTop(i + 1) - 1 : top + 36; + + // Subtle highlight overlay so text/icon remain readable + context.fill(left, top, right, bottom, 0x403575E0); + // Accent line on the left for emphasis + context.fill(left, top, left + 2, bottom, 0xFF3575E0); + } + } } From e9a9593bbf1760018277b2962b5e500fb9cd7472 Mon Sep 17 00:00:00 2001 From: Aries Powvalla Date: Wed, 13 Aug 2025 21:58:44 -0700 Subject: [PATCH 3/5] Multiversion fix, test class removal --- .../java/cc/aabss/eventutils/EventUtils.java | 36 +++++++++++-------- .../mixin/EntryListWidgetAccessor.java | 13 +++++++ .../mixin/MultiplayerScreenMixin.java | 7 ++-- src/main/resources/eventutils.mixin.json | 1 + 4 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 src/main/java/cc/aabss/eventutils/mixin/EntryListWidgetAccessor.java diff --git a/src/main/java/cc/aabss/eventutils/EventUtils.java b/src/main/java/cc/aabss/eventutils/EventUtils.java index 2de3804..0ef4bee 100644 --- a/src/main/java/cc/aabss/eventutils/EventUtils.java +++ b/src/main/java/cc/aabss/eventutils/EventUtils.java @@ -95,11 +95,15 @@ public void onInitializeClient() { InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, "key.category.eventutils")); - final KeyBinding testEventKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( - "key.eventutils.testevent", - InputUtil.Type.KEYSYM, - GLFW.GLFW_KEY_SEMICOLON, - "key.category.eventutils")); + +// DEV ICC: Enable to force test event + +// final KeyBinding testEventKey = KeyBindingHelper.registerKeyBinding(new KeyBinding( +// "key.eventutils.testevent", +// InputUtil.Type.KEYSYM, +// GLFW.GLFW_KEY_SEMICOLON, +// "key.category.eventutils")); + ClientTickEvents.END_CLIENT_TICK.register(client -> { // Hide players key if (hidePlayersKey.wasPressed()) { @@ -117,16 +121,18 @@ public void onInitializeClient() { if (client.player != null) client.player.sendMessage(Text.literal("No event has happened recently!").formatted(Formatting.RED), true); } - // Test event key - if (testEventKey.wasPressed()) { - simulateTestEvent(); - if (client.player != null) { - client.player.sendMessage(Text.literal("Test event simulated! Check your server list and you should see a toast notification.").formatted(Formatting.GREEN), true); - } else { - // In main menu, just log it - LOGGER.info("Test event simulated from main menu"); - } - } +// DEV ICC: Enable to force test event + +// if (testEventKey.wasPressed()) { +// simulateTestEvent(); +// if (client.player != null) { +// client.player.sendMessage(Text.literal("Test event simulated! Check your server list and you should see a toast notification.").formatted(Formatting.GREEN), true); +// } else { +// // In main menu, just log it +// LOGGER.info("Test event simulated from main menu"); +// } +// } + }); // Simple queue message diff --git a/src/main/java/cc/aabss/eventutils/mixin/EntryListWidgetAccessor.java b/src/main/java/cc/aabss/eventutils/mixin/EntryListWidgetAccessor.java new file mode 100644 index 0000000..c5e7f53 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/mixin/EntryListWidgetAccessor.java @@ -0,0 +1,13 @@ +package cc.aabss.eventutils.mixin; + +import net.minecraft.client.gui.widget.EntryListWidget; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(EntryListWidget.class) +public interface EntryListWidgetAccessor { + @Invoker("getRowTop") + int invokeGetRowTop(int index); +} + + diff --git a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java index b3e934f..24c8182 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java @@ -12,6 +12,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.EntryListWidget; @Mixin(MultiplayerScreen.class) public class MultiplayerScreenMixin { @@ -43,8 +44,10 @@ private void highlightEventRows(DrawContext context, int mouseX, int mouseY, flo final boolean isEvent = label.contains(EventServerManager.EVENT_SERVER_PREFIX) || normalized.contains("[Event] "); if (!isEvent) continue; - final int top = serverListWidget.getRowTop(i); - final int bottom = (i + 1 < n) ? serverListWidget.getRowTop(i + 1) - 1 : top + 36; + final int top = ((EntryListWidgetAccessor)(EntryListWidget) serverListWidget).invokeGetRowTop(i); + final int bottom = (i + 1 < n) + ? ((EntryListWidgetAccessor)(EntryListWidget) serverListWidget).invokeGetRowTop(i + 1) - 1 + : top + 36; // Subtle highlight overlay so text/icon remain readable context.fill(left, top, right, bottom, 0x403575E0); diff --git a/src/main/resources/eventutils.mixin.json b/src/main/resources/eventutils.mixin.json index 2b06b48..fd75469 100644 --- a/src/main/resources/eventutils.mixin.json +++ b/src/main/resources/eventutils.mixin.json @@ -7,6 +7,7 @@ "EntityMixin" ], "client": [ + "EntryListWidgetAccessor", "ButtonWidgetMixin", "ClientMixin", "EntityRenderDispatcherMixin", From d1a11af0d9bd099f555b86a3d2c87d4ce092c644 Mon Sep 17 00:00:00 2001 From: srnyx <25808801+srnyx@users.noreply.github.com> Date: Thu, 14 Aug 2025 01:36:12 -0400 Subject: [PATCH 4/5] Delete API_REFERENCE.md No longer needed as API documentation is accessible via https://eventalerts.gg/api now --- API_REFERENCE.md | 110 ----------------------------------------------- 1 file changed, 110 deletions(-) delete mode 100644 API_REFERENCE.md diff --git a/API_REFERENCE.md b/API_REFERENCE.md deleted file mode 100644 index 1f7051e..0000000 --- a/API_REFERENCE.md +++ /dev/null @@ -1,110 +0,0 @@ -# Type Examples -- **String:** `hello` -- **Integer:** `2023` -- **Long:** `242385234992037888` -- **Array:** `element1,element2,element3` -- **Map:** `{key1:"value1",key2:"value2",key3:"value3"}` -# Database/static -**API Prefix:** https://eventalerts.gg/api/v1/ENDPOINT -## Comparisons -- `equals`: The document's value must be the same as the given one -- `contains`: The document's value must contain all of the given ones -## Misc -- `unique`: Only 1 document can have this value, so only 1 will be returned, always (unless something goes terribly wrong) -## Endpoints -- `servers` - - **Example:** https://eventalerts.gg/api/v1/servers?representatives=242385234992037888,365630764244664320&name=test - Will return servers with the name `test` that have srnyx *and* Oiiink as representatives - - **Query Parameters** - - `id`, string, equals, unique - - `message`, long, equals, unique - - `name`, string, equals - - `description`, string, equals - - `invite`, string, equals - - `created`, long, equals - - `tags`, string array, contains - - `color`, integer, equals - - `thumbnail`, string, equals - - `gets`, string array, contains key - - `representatives`, long array, contains -- `players` - - **Example:** https://eventalerts.gg/api/v1/players?boosterPasses=314853603695394817 - Will return any user that has given Skeppy a Booster Pass (should just be one, as you can only get a Booster Pass from one person at a time) - - **Query Parameters** - - `id`, string, equals, unique - - `user`, long, equals, unique - - `anniversaries`, integer array, contains - - `boosterPasses`, long array, contains -- `events` - - **Example:** https://eventalerts.gg/api/v1/events?channel=980956946075115570 - Will get all active events in the Partner events channel - - **Query Parameters** - - `id`, string, equals, unique - - `thread`, long, equals, unique - - `channel`, long, equals - - `title`, string, equals - - `time`, long, equals - - `subscribers`, long array, contains -# Websockets -**API Prefix:** `wss://eventalerts.venox.network/api/v1/socket/SOCKET` -*You must use `wss://`, not `ws://`, as our sockets are encrypted!* -## Conditions -- `builder`: Only included if the event was posted using `/event builder` -## Endpoints -- `server_enabled` - - **Keys** - - `id`, string - - `representatives`, long array - - `created`, long - - `name`, string - - `description`, string - - `invite`, string - - `tags`, string array - - `color`, integer - - `thumbnail`, string - - `message`, long - - `gets`, [string, string] map -- `server_edited` - - **Keys** - - `id`, string - - `representatives`, long array - - `created`, long - - `name`, string - - `description`, string - - `invite`, string - - `tags`, string array - - `color`, integer - - `thumbnail`, string - - `message`, long - - `gets`, [string, string] map -- `booster_pass_given` - - **Keys** - - `id`, string - - `representatives`, long array - - `created`, long - - `name`, string - - `description`, string - - `invite`, string - - `tags`, string array - - `color`, integer - - `thumbnail`, string - - `message`, long - - `gets`, [string, string] map -- `event_posted` - - **Keys** - - `id`, string - - `user`, long - - `anniversaries`, integer array - - `booster_passes`, long array -- `event_cancelled` - - **Keys** - - `channel`, long - - `thread`, long - - `id`, string - - `title`, string - - `time`, long, builder - - `subscribers`, long array, builder -- `potential_famous_event` - - This will just be a string of the message that contained the role ping -- `famous_event` - - This will just be a string of the message that contained the role ping \ No newline at end of file From 9ccbdbda6190e02ac0a486c823d9c456c803c6c8 Mon Sep 17 00:00:00 2001 From: Aries Powvalla Date: Thu, 14 Aug 2025 10:50:51 -0700 Subject: [PATCH 5/5] fix: catch safety, consolidation --- .../aabss/eventutils/EventServerManager.java | 67 ++++++++----------- .../java/cc/aabss/eventutils/EventUtils.java | 11 +-- .../mixin/MultiplayerScreenMixin.java | 1 - .../eventutils/utility/ConnectUtility.java | 49 ++++++++++++++ .../eventutils/websocket/SocketEndpoint.java | 6 +- 5 files changed, 84 insertions(+), 50 deletions(-) diff --git a/src/main/java/cc/aabss/eventutils/EventServerManager.java b/src/main/java/cc/aabss/eventutils/EventServerManager.java index cf8207c..7dea658 100644 --- a/src/main/java/cc/aabss/eventutils/EventServerManager.java +++ b/src/main/java/cc/aabss/eventutils/EventServerManager.java @@ -36,13 +36,35 @@ public void addEventServer(@NotNull JsonObject eventJson) { final MinecraftClient client = MinecraftClient.getInstance(); if (client == null) return; - // Extract event information - final String eventId = eventJson.has("id") ? eventJson.get("id").getAsString() : ("event-" + System.currentTimeMillis()); - final String title = eventJson.has("title") ? eventJson.get("title").getAsString() : "Event"; - final long eventTime = eventJson.has("time") ? eventJson.get("time").getAsLong() : 0; + // Requires precursor variable due to lambda in java 21 + String eventIdPrec = "event-" + System.currentTimeMillis(); + if (eventJson.has("id")) try { + eventIdPrec = eventJson.get("id").getAsString(); + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse ID from event: {}", eventJson, e); + } + final String eventId = eventIdPrec != null && !eventIdPrec.isEmpty() ? eventIdPrec : "event-" + System.currentTimeMillis(); + + // Requires precursor variable due to lambda in java 21 + String titlePrec = "Event"; + if (eventJson.has("title")) try { + titlePrec = eventJson.get("title").getAsString(); + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse title from event: {}", eventJson, e); + } + final String title = titlePrec != null && !titlePrec.isEmpty() ? titlePrec : "Event"; + + // Requires precursor variable due to lambda in java 21 + long eventTimePrec = 0L; + if (eventJson.has("time")) try { + eventTimePrec = eventJson.get("time").getAsLong(); + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse time from event: {}", eventJson, e); + } + final long eventTime = eventTimePrec > 0 ? eventTimePrec : System.currentTimeMillis(); // Try to extract server IP from various possible fields - String serverIp = extractServerIp(eventJson); + String serverIp = ConnectUtility.extractIp(eventJson); if (serverIp == null || serverIp.isEmpty()) { EventUtils.LOGGER.warn("No server IP found for event: {}", title); return; @@ -175,40 +197,7 @@ public void removeAllEventServers() { } } - @Nullable - private String extractServerIp(@NotNull JsonObject eventJson) { - // Try to get IP from common fields in the event JSON - if (eventJson.has("ip")) { - return eventJson.get("ip").getAsString(); - } - if (eventJson.has("server")) { - return eventJson.get("server").getAsString(); - } - if (eventJson.has("address")) { - return eventJson.get("address").getAsString(); - } - - // Try to extract from description using the existing utility - if (eventJson.has("description")) { - final String description = eventJson.get("description").getAsString(); - final String extractedIp = ConnectUtility.getIp(description); - if (extractedIp != null && !extractedIp.isEmpty()) { - return extractedIp; - } - } - - // Try to extract from title as well - if (eventJson.has("title")) { - final String title = eventJson.get("title").getAsString(); - final String extractedIp = ConnectUtility.getIp(title); - if (extractedIp != null && !extractedIp.isEmpty()) { - return extractedIp; - } - } - - // No IP could be determined - return null; - } + public int getActiveEventCount() { return activeEventServers.size(); diff --git a/src/main/java/cc/aabss/eventutils/EventUtils.java b/src/main/java/cc/aabss/eventutils/EventUtils.java index 0ef4bee..71cd445 100644 --- a/src/main/java/cc/aabss/eventutils/EventUtils.java +++ b/src/main/java/cc/aabss/eventutils/EventUtils.java @@ -170,16 +170,11 @@ public String getIpAndConnect(@NotNull EventType eventType, @NotNull JsonObject return ip; } - // Get IP + // Get IP (unified extraction with safe fallbacks) String ip = "hypixel.net"; if (eventType != EventType.HOUSING) { - final JsonElement messageIp = message.get("ip"); - if (messageIp != null) { // Specifically provided - ip = messageIp.getAsString(); - } else { // Extract from description - final JsonElement messageDescription = message.get("description"); - if (messageDescription != null) ip = ConnectUtility.getIp(messageDescription.getAsString()); - } + final String extracted = ConnectUtility.extractIp(message); + if (extracted != null && !extracted.isEmpty()) ip = extracted; } // Auto TP if enabled diff --git a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java index 24c8182..dbe5129 100644 --- a/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java +++ b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java @@ -4,7 +4,6 @@ import cc.aabss.eventutils.EventServerManager; import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen; import net.minecraft.client.option.ServerList; -import net.minecraft.client.network.ServerInfo; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java b/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java index e4e43ea..24554d1 100644 --- a/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java +++ b/src/main/java/cc/aabss/eventutils/utility/ConnectUtility.java @@ -1,6 +1,7 @@ package cc.aabss.eventutils.utility; import cc.aabss.eventutils.EventUtils; +import com.google.gson.JsonObject; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.TitleScreen; import net.minecraft.client.gui.screen.multiplayer.ConnectScreen; @@ -45,6 +46,54 @@ public static void connect(@NotNull String ip) { }); } + @Nullable + public static String extractIp(@NotNull JsonObject eventJson) { + // Direct IP field + if (eventJson.has("ip")) try { + final String ip = eventJson.get("ip").getAsString(); + if (ip != null && !ip.isEmpty()) return ip; + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse ip from event: {}", eventJson, e); + } + + // Extract from description + if (eventJson.has("description")) try { + final String description = eventJson.get("description").getAsString(); + final String extracted = getIp(description); + if (extracted != null && !extracted.isEmpty()) return extracted; + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse description for IP from event: {}", eventJson, e); + } + + // Extract from title + if (eventJson.has("title")) try { + final String title = eventJson.get("title").getAsString(); + final String extracted = getIp(title); + if (extracted != null && !extracted.isEmpty()) return extracted; + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse title for IP from event: {}", eventJson, e); + } + + // Extract from message + if (eventJson.has("message")) try { + final String message = eventJson.get("message").getAsString(); + final String extracted = getIp(message); + if (extracted != null && !extracted.isEmpty()) return extracted; + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse message for IP from event: {}", eventJson, e); + } + + // Last resort: address (may not exist and could mean something else) + if (eventJson.has("address")) try { + final String address = eventJson.get("address").getAsString(); + if (address != null && !address.isEmpty()) return address; + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse address from event: {}", eventJson, e); + } + + return null; + } + @Nullable public static String getIp(@NotNull String event) { // Get strings diff --git a/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java b/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java index 5fa52e8..62e2436 100644 --- a/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java +++ b/src/main/java/cc/aabss/eventutils/websocket/SocketEndpoint.java @@ -61,10 +61,12 @@ public enum SocketEndpoint { final JsonObject json = parseJson(message); if (json == null) return; - // Remove event server from server list if it exists - if (json.has("id")) { + // Remove event server from server list if it exists (safe parsing) + if (json.has("id")) try { final String eventId = json.get("id").getAsString(); mod.eventServerManager.removeEventServer(eventId); + } catch (final Exception e) { + EventUtils.LOGGER.warn("Failed to parse ID from cancellation event: {}", json, e); } });