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..7dea658 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/EventServerManager.java @@ -0,0 +1,230 @@ +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 { + public static final String EVENT_SERVER_PREFIX = "§7[Event] §r"; + + @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; + + // 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 = ConnectUtility.extractIp(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 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 5 minutes after event starts + if (eventTime > 0) { + final long currentTime = System.currentTimeMillis(); + final long graceMs = TimeUnit.MINUTES.toMillis(5); + final long timeUntilRemoval = (eventTime + graceMs) - currentTime; + + if (timeUntilRemoval > 0) { + final ScheduledFuture removalTask = mod.scheduler.schedule( + () -> removeEventServer(eventId), + timeUntilRemoval, + TimeUnit.MILLISECONDS + ); + removalTasks.put(eventId, removalTask); + EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms (5m after start)", title, timeUntilRemoval); + } else { + // 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; + } + } + } + + // 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); + } + + // 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); + } + } + + + + public int getActiveEventCount() { + return activeEventServers.size(); + } + + 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..71cd445 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,15 @@ public void onInitializeClient() { InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, "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()) { @@ -108,6 +120,19 @@ public void onInitializeClient() { } if (client.player != null) client.player.sendMessage(Text.literal("No event has happened recently!").formatted(Formatting.RED), true); } + +// 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 @@ -145,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 @@ -177,4 +197,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/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 new file mode 100644 index 0000000..dbe5129 --- /dev/null +++ b/src/main/java/cc/aabss/eventutils/mixin/MultiplayerScreenMixin.java @@ -0,0 +1,57 @@ +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 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; +import net.minecraft.client.gui.widget.EntryListWidget; + +@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) { + // Store reference to server list for EventServerManager + if (EventUtils.MOD != null) { + 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 = ((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); + // Accent line on the left for emphasis + context.fill(left, top, left + 2, bottom, 0xFF3575E0); + } + } +} 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 3ac6b60..62e2436 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,19 @@ 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 (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); + } }); @Nullable public static JsonObject LAST_EVENT; diff --git a/src/main/resources/eventutils.mixin.json b/src/main/resources/eventutils.mixin.json index ad911e2..fd75469 100644 --- a/src/main/resources/eventutils.mixin.json +++ b/src/main/resources/eventutils.mixin.json @@ -7,9 +7,11 @@ "EntityMixin" ], "client": [ + "EntryListWidgetAccessor", "ButtonWidgetMixin", "ClientMixin", "EntityRenderDispatcherMixin", + "MultiplayerScreenMixin", "PlayerEntityRendererMixin" ], "injectors": {