Files
SCOPENET-MC/integrations/common/src/main/java/net/scopenet/integration/Activity.java
T
SCOPEDD a1f19e86c6 Complete private authentication, server integrations and activity reporting
Add Fabric/Forge version builds and Paper integration, preserve permanent player identities across renames, harden session authorization, and surface privacy-conscious launcher/server activity in the panel.
2026-09-28 13:31:17 -04:00

118 lines
5.1 KiB
Java

package net.scopenet.integration;
import com.google.gson.*;
import java.util.*;
import java.util.function.LongSupplier;
/** All callers (including asynchronous chat events) share one short lock. */
public final class Activity {
private record Player(String name, long since) {}
private final Map<UUID, Player> online = new LinkedHashMap<>();
private final Map<UUID, JsonObject> stats = new LinkedHashMap<>();
private final List<JsonObject> events = new ArrayList<>();
private final Map<String, JsonObject> actions = new LinkedHashMap<>();
private long dropped;
private final LongSupplier clock;
private static final int MAX_PLAYERS = 5000;
private static final int MAX_EVENTS = 500;
public Activity() { this(System::nanoTime); }
Activity(LongSupplier clock) { this.clock = clock; }
public synchronized void join(UUID id, String name) {
if (online.containsKey(id)) return;
if (online.size() >= MAX_PLAYERS) return;
online.put(id, new Player(name, clock.getAsLong()));
add(id, name, "joins", 1);
event(id, name, "join", null);
}
public synchronized void leave(UUID id, String name) {
Player player = online.remove(id);
if (player == null) return;
add(id, name, "playtime_secs", Math.max(0, (clock.getAsLong() - player.since()) / 1_000_000_000L));
event(id, name, "leave", null);
}
public synchronized void add(UUID id, String name, String key, long count) {
if (count <= 0 || (!stats.containsKey(id) && stats.size() >= MAX_PLAYERS)) return;
JsonObject row = stats.computeIfAbsent(id, unused -> identity(id, name));
row.addProperty(key, Math.min(1_000_000L, (row.has(key) ? row.get(key).getAsLong() : 0) + count));
}
public synchronized void event(UUID id, String name, String kind, String detail) {
if (events.size() >= 10_000) { dropped++; return; }
JsonObject event = identity(id, name);
event.addProperty("kind", kind);
if (detail != null) event.addProperty("detail", detail.substring(0, Math.min(256, detail.length())));
event.addProperty("at", System.currentTimeMillis());
events.add(event);
}
/** Repeated vanilla actions are aggregated within each reporting interval. */
public synchronized void action(UUID id, String name, String action, long count) {
String key = id + ":" + action;
if (count <= 0) return;
if (!actions.containsKey(key) && actions.size() >= 10_000) { dropped++; return; }
JsonObject row = actions.computeIfAbsent(key, unused -> {
JsonObject value = identity(id, name);
value.addProperty("kind", "action");
value.addProperty("action", action);
value.addProperty("count", 0);
value.addProperty("at", System.currentTimeMillis());
return value;
});
row.addProperty("count", row.get("count").getAsLong() + count);
}
public synchronized JsonObject drain(double tps) {
long now = clock.getAsLong();
JsonArray players = new JsonArray();
online.replaceAll((id, player) -> {
long seconds = Math.max(0, (now - player.since()) / 1_000_000_000L);
add(id, player.name(), "playtime_secs", seconds);
players.add(identity(id, player.name()));
return new Player(player.name(), player.since() + seconds * 1_000_000_000L);
});
JsonObject payload = new JsonObject();
payload.addProperty("batch_id", UUID.randomUUID().toString());
payload.addProperty("tps", Double.isFinite(tps) ? Math.max(0, Math.min(20, tps)) : 20);
payload.add("online", players);
JsonArray rows = new JsonArray();
stats.values().forEach(rows::add);
payload.add("stats", rows);
JsonArray activity = new JsonArray();
int eventCount = Math.min(events.size(), MAX_EVENTS - 1);
events.subList(0, eventCount).forEach(activity::add);
events.subList(0, eventCount).clear();
var iterator = actions.values().iterator();
while (iterator.hasNext() && activity.size() < MAX_EVENTS - 1) {
JsonObject action = iterator.next();
action.addProperty("detail", action.remove("action").getAsString() + " +" + action.remove("count").getAsLong());
activity.add(action);
iterator.remove();
}
if (dropped > 0) {
JsonObject gap = new JsonObject();
gap.addProperty("kind", "telemetry_gap");
gap.addProperty("detail", dropped + " events exceeded the local queue capacity");
activity.add(gap);
dropped = 0;
}
payload.add("events", activity);
stats.clear();
return payload;
}
public synchronized void leaveAll() {
new LinkedHashMap<>(online).forEach((id, player) -> leave(id, player.name()));
}
private static JsonObject identity(UUID id, String name) {
JsonObject object = new JsonObject();
object.addProperty("uuid", id.toString());
object.addProperty("name", name);
return object;
}
}