Skip to content

Commit 2ba3deb

Browse files
authored
Merge pull request #14 from ariesninjadev/dev/events_in_list
Add upcoming events to (MC) Server List
2 parents 414fae6 + 9ccbdbd commit 2ba3deb

10 files changed

Lines changed: 434 additions & 12 deletions

File tree

build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ dependencies {
2525
minecraft("com.mojang", "minecraft", property("deps.minecraft").toString())
2626
mappings("net.fabricmc", "yarn", property("deps.yarn_mappings").toString())
2727

28-
modCompileOnly("net.fabricmc", "fabric-loader", property("deps.fabric_loader").toString())
29-
modCompileOnly("net.fabricmc.fabric-api", "fabric-api", property("deps.fabric_api").toString())
28+
modImplementation("net.fabricmc", "fabric-loader", property("deps.fabric_loader").toString())
29+
modImplementation("net.fabricmc.fabric-api", "fabric-api", property("deps.fabric_api").toString())
3030

31-
modCompileOnly("dev.isxander", "yet-another-config-lib", property("deps.yacl").toString())
32-
modCompileOnly("com.terraformersmc", "modmenu", property("deps.modmenu").toString())
31+
modImplementation("dev.isxander", "yet-another-config-lib", property("deps.yacl").toString())
32+
modImplementation("com.terraformersmc", "modmenu", property("deps.modmenu").toString())
3333
}
3434

3535
// Add placeholder-api dependency if property exists

gradlew

100644100755
File mode changed.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package cc.aabss.eventutils;
2+
3+
import cc.aabss.eventutils.utility.ConnectUtility;
4+
5+
import net.minecraft.client.MinecraftClient;
6+
import net.minecraft.client.network.ServerInfo;
7+
import net.minecraft.client.option.ServerList;
8+
9+
import com.google.gson.JsonObject;
10+
11+
import org.jetbrains.annotations.NotNull;
12+
import org.jetbrains.annotations.Nullable;
13+
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
import java.util.concurrent.ScheduledFuture;
17+
import java.util.concurrent.TimeUnit;
18+
19+
public class EventServerManager {
20+
public static final String EVENT_SERVER_PREFIX = "§7[Event] §r";
21+
22+
@NotNull private final EventUtils mod;
23+
@NotNull private final Map<String, EventServerInfo> activeEventServers = new HashMap<>();
24+
@NotNull private final Map<String, ScheduledFuture<?>> removalTasks = new HashMap<>();
25+
@Nullable private ServerList serverList;
26+
27+
public EventServerManager(@NotNull EventUtils mod) {
28+
this.mod = mod;
29+
}
30+
31+
public void setServerList(@Nullable ServerList serverList) {
32+
this.serverList = serverList;
33+
}
34+
35+
public void addEventServer(@NotNull JsonObject eventJson) {
36+
final MinecraftClient client = MinecraftClient.getInstance();
37+
if (client == null) return;
38+
39+
// Requires precursor variable due to lambda in java 21
40+
String eventIdPrec = "event-" + System.currentTimeMillis();
41+
if (eventJson.has("id")) try {
42+
eventIdPrec = eventJson.get("id").getAsString();
43+
} catch (final Exception e) {
44+
EventUtils.LOGGER.warn("Failed to parse ID from event: {}", eventJson, e);
45+
}
46+
final String eventId = eventIdPrec != null && !eventIdPrec.isEmpty() ? eventIdPrec : "event-" + System.currentTimeMillis();
47+
48+
// Requires precursor variable due to lambda in java 21
49+
String titlePrec = "Event";
50+
if (eventJson.has("title")) try {
51+
titlePrec = eventJson.get("title").getAsString();
52+
} catch (final Exception e) {
53+
EventUtils.LOGGER.warn("Failed to parse title from event: {}", eventJson, e);
54+
}
55+
final String title = titlePrec != null && !titlePrec.isEmpty() ? titlePrec : "Event";
56+
57+
// Requires precursor variable due to lambda in java 21
58+
long eventTimePrec = 0L;
59+
if (eventJson.has("time")) try {
60+
eventTimePrec = eventJson.get("time").getAsLong();
61+
} catch (final Exception e) {
62+
EventUtils.LOGGER.warn("Failed to parse time from event: {}", eventJson, e);
63+
}
64+
final long eventTime = eventTimePrec > 0 ? eventTimePrec : System.currentTimeMillis();
65+
66+
// Try to extract server IP from various possible fields
67+
String serverIp = ConnectUtility.extractIp(eventJson);
68+
if (serverIp == null || serverIp.isEmpty()) {
69+
EventUtils.LOGGER.warn("No server IP found for event: {}", title);
70+
return;
71+
}
72+
73+
// Don't add if already exists (fast-path check)
74+
if (activeEventServers.containsKey(eventId)) return;
75+
76+
client.execute(() -> {
77+
if (!ensureServerListLoaded()) {
78+
EventUtils.LOGGER.warn("Server list not available, cannot add event server");
79+
return;
80+
}
81+
82+
// Create server info
83+
final String serverName = EVENT_SERVER_PREFIX + title;
84+
final ServerInfo serverInfo = new ServerInfo(serverName, serverIp, ServerInfo.ServerType.OTHER);
85+
serverInfo.setResourcePackPolicy(ServerInfo.ResourcePackPolicy.PROMPT);
86+
87+
// Add the server to the list (avoid duplicates in the persistent list)
88+
for (int i = 0; i < serverList.size(); i++) {
89+
final ServerInfo existing = serverList.get(i);
90+
if (existing.name.equals(serverName) && existing.address.equalsIgnoreCase(serverIp)) {
91+
EventUtils.LOGGER.info("Event server already present in server list: '{}' -> '{}'", serverName, serverIp);
92+
return;
93+
}
94+
}
95+
serverList.add(serverInfo, false);
96+
97+
// Store event server info
98+
final EventServerInfo eventServerInfo = new EventServerInfo(eventId, serverInfo, eventTime);
99+
activeEventServers.put(eventId, eventServerInfo);
100+
101+
// Schedule removal 5 minutes after event starts
102+
if (eventTime > 0) {
103+
final long currentTime = System.currentTimeMillis();
104+
final long graceMs = TimeUnit.MINUTES.toMillis(5);
105+
final long timeUntilRemoval = (eventTime + graceMs) - currentTime;
106+
107+
if (timeUntilRemoval > 0) {
108+
final ScheduledFuture<?> removalTask = mod.scheduler.schedule(
109+
() -> removeEventServer(eventId),
110+
timeUntilRemoval,
111+
TimeUnit.MILLISECONDS
112+
);
113+
removalTasks.put(eventId, removalTask);
114+
EventUtils.LOGGER.info("Scheduled removal of event server '{}' in {} ms (5m after start)", title, timeUntilRemoval);
115+
} else {
116+
// If within 5-minute grace after event start, keep it briefly; else do not add
117+
if (currentTime - eventTime <= graceMs) {
118+
final long remaining = graceMs - (currentTime - eventTime);
119+
final ScheduledFuture<?> removalTask = mod.scheduler.schedule(
120+
() -> removeEventServer(eventId),
121+
remaining,
122+
TimeUnit.MILLISECONDS
123+
);
124+
removalTasks.put(eventId, removalTask);
125+
EventUtils.LOGGER.info("Event '{}' already started; keeping for {} ms (grace)", title, remaining);
126+
} else {
127+
serverList.remove(serverInfo);
128+
activeEventServers.remove(eventId);
129+
EventUtils.LOGGER.info("Event '{}' started more than 5 minutes ago; not adding", title);
130+
return;
131+
}
132+
}
133+
}
134+
135+
// Persist changes to disk so they show up when user opens the Multiplayer screen later
136+
try {
137+
serverList.saveFile();
138+
} catch (final Exception e) {
139+
EventUtils.LOGGER.error("Failed to save server list after adding event server", e);
140+
}
141+
142+
EventUtils.LOGGER.info("Added event server '{}' with IP '{}' to server list", title, serverIp);
143+
});
144+
}
145+
146+
public void removeEventServer(@NotNull String eventId) {
147+
final MinecraftClient client = MinecraftClient.getInstance();
148+
if (client == null) return;
149+
client.execute(() -> {
150+
final EventServerInfo eventServerInfo = activeEventServers.remove(eventId);
151+
if (eventServerInfo == null) return;
152+
153+
if (!ensureServerListLoaded()) {
154+
EventUtils.LOGGER.warn("Server list not available, cannot remove event server");
155+
return;
156+
}
157+
158+
// Remove from server list by matching properties (instance may differ if server list was reloaded)
159+
int removedCount = 0;
160+
for (int i = serverList.size() - 1; i >= 0; i--) {
161+
final ServerInfo candidate = serverList.get(i);
162+
if (candidate.name.equals(eventServerInfo.serverInfo.name)
163+
&& candidate.address.equalsIgnoreCase(eventServerInfo.serverInfo.address)) {
164+
serverList.remove(candidate);
165+
removedCount++;
166+
}
167+
}
168+
if (removedCount == 0) {
169+
EventUtils.LOGGER.warn("Event server not found in current server list for removal: '{}' -> '{}'", eventServerInfo.serverInfo.name, eventServerInfo.serverInfo.address);
170+
}
171+
172+
// Cancel removal task
173+
final ScheduledFuture<?> removalTask = removalTasks.remove(eventId);
174+
if (removalTask != null) {
175+
removalTask.cancel(false);
176+
}
177+
178+
// Persist removal
179+
try {
180+
serverList.saveFile();
181+
} catch (final Exception e) {
182+
EventUtils.LOGGER.error("Failed to save server list after removing event server", e);
183+
}
184+
185+
EventUtils.LOGGER.info("Removed event server from server list: {}", eventServerInfo.serverInfo.name);
186+
});
187+
}
188+
189+
public void removeAllEventServers() {
190+
// Cancel all removal tasks
191+
removalTasks.values().forEach(task -> task.cancel(false));
192+
removalTasks.clear();
193+
194+
// Remove all event servers
195+
for (final String eventId : new HashMap<>(activeEventServers).keySet()) {
196+
removeEventServer(eventId);
197+
}
198+
}
199+
200+
201+
202+
public int getActiveEventCount() {
203+
return activeEventServers.size();
204+
}
205+
206+
private boolean ensureServerListLoaded() {
207+
if (this.serverList != null) return true;
208+
final MinecraftClient client = MinecraftClient.getInstance();
209+
if (client == null) return false;
210+
this.serverList = new ServerList(client);
211+
try {
212+
this.serverList.loadFile();
213+
} catch (final Exception e) {
214+
EventUtils.LOGGER.error("Failed to load server list", e);
215+
}
216+
return true;
217+
}
218+
219+
private static class EventServerInfo {
220+
@NotNull public final String eventId;
221+
@NotNull public final ServerInfo serverInfo;
222+
public final long eventTime;
223+
224+
public EventServerInfo(@NotNull String eventId, @NotNull ServerInfo serverInfo, long eventTime) {
225+
this.eventId = eventId;
226+
this.serverInfo = serverInfo;
227+
this.eventTime = eventTime;
228+
}
229+
}
230+
}

src/main/java/cc/aabss/eventutils/EventUtils.java

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public class EventUtils implements ClientModInitializer {
5656
@NotNull public final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
5757
@NotNull public final Set<WebSocketClient> webSockets = new HashSet<>();
5858
@NotNull public final UpdateChecker updateChecker = new UpdateChecker(this);
59+
@NotNull public final EventServerManager eventServerManager = new EventServerManager(this);
5960
@NotNull public final Map<EventType, String> lastIps = new EnumMap<>(EventType.class);
6061
public boolean hidePlayers = false;
6162

@@ -69,13 +70,15 @@ public void onInitializeClient() {
6970
// Websockets
7071
webSockets.add(new WebSocketClient(this, SocketEndpoint.EVENT_POSTED));
7172
webSockets.add(new WebSocketClient(this, SocketEndpoint.FAMOUS_EVENT_POSTED));
73+
webSockets.add(new WebSocketClient(this, SocketEndpoint.EVENT_CANCELLED));
7274

7375
// Command registration
7476
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> CommandRegister.register(dispatcher));
7577

7678
// Game closed
7779
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> {
7880
webSockets.forEach(socket -> socket.close("Game closed"));
81+
eventServerManager.removeAllEventServers();
7982
});
8083

8184
// Update checker
@@ -92,6 +95,15 @@ public void onInitializeClient() {
9295
InputUtil.Type.KEYSYM,
9396
GLFW.GLFW_KEY_RIGHT_SHIFT,
9497
"key.category.eventutils"));
98+
99+
// DEV ICC: Enable to force test event
100+
101+
// final KeyBinding testEventKey = KeyBindingHelper.registerKeyBinding(new KeyBinding(
102+
// "key.eventutils.testevent",
103+
// InputUtil.Type.KEYSYM,
104+
// GLFW.GLFW_KEY_SEMICOLON,
105+
// "key.category.eventutils"));
106+
95107
ClientTickEvents.END_CLIENT_TICK.register(client -> {
96108
// Hide players key
97109
if (hidePlayersKey.wasPressed()) {
@@ -108,6 +120,19 @@ public void onInitializeClient() {
108120
}
109121
if (client.player != null) client.player.sendMessage(Text.literal("No event has happened recently!").formatted(Formatting.RED), true);
110122
}
123+
124+
// DEV ICC: Enable to force test event
125+
126+
// if (testEventKey.wasPressed()) {
127+
// simulateTestEvent();
128+
// if (client.player != null) {
129+
// client.player.sendMessage(Text.literal("Test event simulated! Check your server list and you should see a toast notification.").formatted(Formatting.GREEN), true);
130+
// } else {
131+
// // In main menu, just log it
132+
// LOGGER.info("Test event simulated from main menu");
133+
// }
134+
// }
135+
111136
});
112137

113138
// Simple queue message
@@ -145,16 +170,11 @@ public String getIpAndConnect(@NotNull EventType eventType, @NotNull JsonObject
145170
return ip;
146171
}
147172

148-
// Get IP
173+
// Get IP (unified extraction with safe fallbacks)
149174
String ip = "hypixel.net";
150175
if (eventType != EventType.HOUSING) {
151-
final JsonElement messageIp = message.get("ip");
152-
if (messageIp != null) { // Specifically provided
153-
ip = messageIp.getAsString();
154-
} else { // Extract from description
155-
final JsonElement messageDescription = message.get("description");
156-
if (messageDescription != null) ip = ConnectUtility.getIp(messageDescription.getAsString());
157-
}
176+
final String extracted = ConnectUtility.extractIp(message);
177+
if (extracted != null && !extracted.isEmpty()) ip = extracted;
158178
}
159179

160180
// Auto TP if enabled
@@ -177,4 +197,35 @@ public static int max(int... values) {
177197
public static String translate(@NotNull String key) {
178198
return Language.getInstance().get(key);
179199
}
200+
201+
/**
202+
* Simulates an event being posted for testing purposes.
203+
* Creates a test event that starts in 5 minutes.
204+
*/
205+
public void simulateTestEvent() {
206+
final long currentTime = System.currentTimeMillis();
207+
final long eventTime = currentTime + (1 * 30 * 1000);
208+
209+
// Create a test event JSON object with the correct structure
210+
final JsonObject testEvent = new JsonObject();
211+
testEvent.addProperty("id", "test-event-" + currentTime);
212+
testEvent.addProperty("title", "Test Event");
213+
testEvent.addProperty("description", "This is a simulated test event for testing the server list feature. Server: mc.hypixel.net");
214+
testEvent.addProperty("time", eventTime);
215+
testEvent.addProperty("ip", "invadedlands.net");
216+
testEvent.addProperty("prize", "$1000");
217+
218+
// Add the rolesNamed array that EventType.fromJson expects
219+
final com.google.gson.JsonArray rolesArray = new com.google.gson.JsonArray();
220+
rolesArray.add("MONEY");
221+
testEvent.add("rolesNamed", rolesArray);
222+
223+
LOGGER.info("Simulating test event: {}", testEvent.toString());
224+
225+
// Process the event through the EVENT_POSTED handler
226+
SocketEndpoint.EVENT_POSTED.handler.accept(this, testEvent.toString());
227+
228+
// Set as last event for event info screen
229+
SocketEndpoint.LAST_EVENT = testEvent;
230+
}
180231
}

src/main/java/cc/aabss/eventutils/commands/TeleportCmd.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public static void teleport(@NotNull CommandContext<FabricClientCommandSource> c
3131
return;
3232
}
3333

34+
System.out.println("Connecting to " + lastIp + " for event " + type.name().toLowerCase());
35+
3436
// Connect
3537
ConnectUtility.connect(lastIp);
3638
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package cc.aabss.eventutils.mixin;
2+
3+
import net.minecraft.client.gui.widget.EntryListWidget;
4+
import org.spongepowered.asm.mixin.Mixin;
5+
import org.spongepowered.asm.mixin.gen.Invoker;
6+
7+
@Mixin(EntryListWidget.class)
8+
public interface EntryListWidgetAccessor {
9+
@Invoker("getRowTop")
10+
int invokeGetRowTop(int index);
11+
}
12+
13+

0 commit comments

Comments
 (0)