Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
230 changes: 230 additions & 0 deletions src/main/java/cc/aabss/eventutils/EventServerManager.java
Original file line number Diff line number Diff line change
@@ -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<String, EventServerInfo> activeEventServers = new HashMap<>();
@NotNull private final Map<String, ScheduledFuture<?>> 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;
}
}
}
67 changes: 59 additions & 8 deletions src/main/java/cc/aabss/eventutils/EventUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class EventUtils implements ClientModInitializer {
@NotNull public final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
@NotNull public final Set<WebSocketClient> webSockets = new HashSet<>();
@NotNull public final UpdateChecker updateChecker = new UpdateChecker(this);
@NotNull public final EventServerManager eventServerManager = new EventServerManager(this);
@NotNull public final Map<EventType, String> lastIps = new EnumMap<>(EventType.class);
public boolean hidePlayers = false;

Expand All @@ -69,13 +70,15 @@ 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));

// Game closed
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> {
webSockets.forEach(socket -> socket.close("Game closed"));
eventServerManager.removeAllEventServers();
});

// Update checker
Expand All @@ -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()) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public static void teleport(@NotNull CommandContext<FabricClientCommandSource> c
return;
}

System.out.println("Connecting to " + lastIp + " for event " + type.name().toLowerCase());

// Connect
ConnectUtility.connect(lastIp);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}


Loading