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
Original file line number Diff line number Diff line change
Expand Up @@ -1342,8 +1342,54 @@ private void loadVersionFile() {
advancedCoreBuildNumber = conf.getString("buildnumber", "NOTSET");
}

private volatile com.bencodez.advancedcore.api.user.presence.PlayerPresenceService playerPresenceService;
private com.bencodez.advancedcore.api.user.presence.BukkitPlayerPresenceLookup playerPresenceLookup;

/**
* Read checked history for an already resolved server UUID, without name/profile
* resolution or storage on the caller. UNKNOWN is retained by display filters.
* Completion may run on a worker; schedule platform effects explicitly.
*/
public java.util.concurrent.CompletionStage<com.bencodez.advancedcore.api.user.presence.PlayerPresence>
checkPlayerPresenceAsync(java.util.UUID identity) {
var service = playerPresenceService;
return service == null
? java.util.concurrent.CompletableFuture.completedStage(
com.bencodez.advancedcore.api.user.presence.PlayerPresence.UNKNOWN)
: service.checkAsync(identity);
}

/** Check verified current/historical UUIDs; unresolved aliases must not be omitted. */
public java.util.concurrent.CompletionStage<com.bencodez.advancedcore.api.user.presence.PlayerPresence>
checkPlayerPresenceIdentitiesAsync(java.util.Collection<java.util.UUID> identities) {
var service = playerPresenceService;
return service == null
? java.util.concurrent.CompletableFuture.completedStage(
com.bencodez.advancedcore.api.user.presence.PlayerPresence.UNKNOWN)
: service.checkIdentitiesAsync(identities);
}

/** Also require proof that no unmapped offline identity can retain earlier history. */
public java.util.concurrent.CompletionStage<com.bencodez.advancedcore.api.user.presence.PlayerPresence>
checkPlayerPresenceIdentitiesAsync(java.util.Collection<java.util.UUID> identities, boolean completeOnlineHistory) {
var service = playerPresenceService;
return service == null
? java.util.concurrent.CompletableFuture.completedStage(
com.bencodez.advancedcore.api.user.presence.PlayerPresence.UNKNOWN)
: service.checkIdentitiesAsync(identities, completeOnlineHistory);
}

@Override
public void onDisable() {
var presence = playerPresenceService;
playerPresenceService = null;
if (presence != null) presence.close();
var lookup = playerPresenceLookup;
playerPresenceLookup = null;
if (lookup != null) {
lookup.close();
org.bukkit.event.HandlerList.unregisterAll(lookup);
}
new com.bencodez.advancedcore.lifecycle.AdvancedCoreLifecycle(this).shutdown();
javaPlugin = null;
}
Expand All @@ -1358,6 +1404,23 @@ public void onEnable() {
loginTimer = runtimeExecutors.getLoginTimer();
advancedCoreCommandLoader = CommandLoader.getInstance();
inventoryTimer = runtimeExecutors.getInventoryTimer();
var previousPresence = playerPresenceService;
if (previousPresence != null) previousPresence.close();
playerPresenceService = null;
var previousLookup = playerPresenceLookup;
if (previousLookup != null) {
previousLookup.close();
org.bukkit.event.HandlerList.unregisterAll(previousLookup);
}
var presenceServer = getServer();
var presenceDirectory = com.bencodez.advancedcore.api.user.presence.BukkitPlayerPresenceLookup
.capturePrimaryHistoryDirectory(presenceServer);
var presenceLookup = new com.bencodez.advancedcore.api.user.presence.BukkitPlayerPresenceLookup(presenceDirectory);
presenceLookup.capturePlayerSaving(presenceServer);
playerPresenceLookup = presenceLookup;
registerEvents(presenceLookup);
presenceLookup.captureJoinedPlayers(presenceServer);
playerPresenceService = new com.bencodez.advancedcore.api.user.presence.PlayerPresenceService(timer, presenceLookup);

onPreLoad();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.bencodez.advancedcore.api.user.presence;

import java.util.List;
import java.util.UUID;

/** Internal cohort optimization; older/custom public lookup implementations remain compatible. */
interface BatchPlayerPresenceLookup extends PlayerPresenceLookup {
record Query(UUID identity, boolean completeOnlineHistory) { }
List<PlayerPresence> checkBatch(List<Query> queries) throws Exception;
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.bencodez.advancedcore.api.user.presence;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.zip.CRC32;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

/** Bounded structural inspection only: no server loader, data fixing or writes. */
final class PlayerHistoryFormat {
static final int MAX_COMPRESSED = 1024 * 1024;
private static final int MAX_EXPANDED = 8 * 1024 * 1024;
private int nodes;

static boolean valid(byte[] compressed) throws IOException {
byte[] payload = expand(compressed);
try (var input = new DataInputStream(new ByteArrayInputStream(payload))) {
if (input.readUnsignedByte() != 10) return false;
input.readUTF();
new PlayerHistoryFormat().tag(input, 10, 0);
return input.read() == -1;
}
}

private static byte[] expand(byte[] bytes) throws IOException {
if (bytes.length < 18 || bytes.length > MAX_COMPRESSED
|| (bytes[0] & 255) != 31 || (bytes[1] & 255) != 139 || bytes[2] != 8)
throw new IOException("Invalid history compression");
int flags = bytes[3] & 255;
if ((flags & 224) != 0) throw new IOException("Invalid history flags");
int offset = 10;
if ((flags & 4) != 0) {
if (offset + 2 > bytes.length - 8) throw new IOException("Truncated history header");
int length = (bytes[offset] & 255) | ((bytes[offset + 1] & 255) << 8);
offset += 2 + length;
}
for (int flag : new int[] {8, 16}) if ((flags & flag) != 0) {
boolean terminated = false;
while (offset < bytes.length - 8) if (bytes[offset++] == 0) { terminated = true; break; }
if (!terminated)
throw new IOException("Truncated history header");
}
if ((flags & 2) != 0) {
if (offset + 2 > bytes.length - 8) throw new IOException("Truncated history header");
var headerCrc = new CRC32(); headerCrc.update(bytes, 0, offset);
int expected = (bytes[offset] & 255) | ((bytes[offset + 1] & 255) << 8);
if ((headerCrc.getValue() & 65535) != expected) throw new IOException("Invalid history header checksum");
offset += 2;
}
if (offset > bytes.length - 8) throw new IOException("Truncated history frame");
var inflater = new Inflater(true);
try {
inflater.setInput(bytes, offset, bytes.length - offset);
var output = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
while (!inflater.finished()) {
int count = inflater.inflate(buffer);
if (count == 0 && !inflater.finished()) throw new IOException("Incomplete history frame");
if (output.size() > MAX_EXPANDED - count) throw new IOException("History expansion limit");
output.write(buffer, 0, count);
}
int trailer = bytes.length - inflater.getRemaining();
if (trailer != bytes.length - 8) throw new IOException("Adjacent history frame data");
byte[] payload = output.toByteArray();
var crc = new CRC32(); crc.update(payload);
if (littleInt(bytes, trailer) != crc.getValue() || littleInt(bytes, trailer + 4) != payload.length)
throw new IOException("Invalid history checksum");
return payload;
} catch (DataFormatException invalid) {
throw new IOException("Invalid history deflate", invalid);
} finally {
inflater.end();
}
}

private static long littleInt(byte[] data, int offset) {
long result = 0;
for (int index = 0; index < 4; index++) result |= (long) (data[offset + index] & 255) << (8 * index);
return result;
}

private void tag(DataInputStream input, int type, int depth) throws IOException {
if (depth > 64 || ++nodes > 100000) throw new IOException("History structure limit");
switch (type) {
case 1 -> input.readByte();
case 2 -> input.readShort();
case 3, 5 -> input.readInt();
case 4, 6 -> input.readLong();
case 7 -> array(input, 1);
case 8 -> input.readUTF();
case 9 -> {
int element = input.readUnsignedByte();
int count = input.readInt();
if (count < 0 || count > 100000 || element > 12 || (element == 0 && count != 0))
throw new IOException("Invalid history list");
for (int index = 0; index < count; index++) tag(input, element, depth + 1);
}
case 10 -> {
var names = new HashSet<String>();
int child;
while ((child = input.readUnsignedByte()) != 0) {
String name = input.readUTF();
if (!names.add(name)) throw new IOException("Duplicate history name");
tag(input, child, depth + 1);
}
}
case 11 -> array(input, 4);
case 12 -> array(input, 8);
default -> throw new IOException("Invalid history tag");
}
}

private void array(DataInputStream input, int width) throws IOException {
int count = input.readInt();
if (count < 0 || count > MAX_EXPANDED / width || (long) count * width > input.available())
throw new IOException("Invalid history array");
input.skipNBytes((long) count * width);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bencodez.advancedcore.api.user.presence;

/** Checked server history; plugin user-row existence is not server-history evidence. */
public enum PlayerPresence {
JOINED,
NEVER_JOINED,
UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bencodez.advancedcore.api.user.presence;

import java.util.UUID;

/**
* A read-only lookup of an already resolved identity on a persistence worker.
* Implementations must return UNKNOWN for incomplete identity/history evidence.
* Absence from a plugin's storage is insufficient evidence for NEVER_JOINED.
*/
@FunctionalInterface
public interface PlayerPresenceLookup {
PlayerPresence check(UUID identity) throws Exception;

/**
* Additionally certify absence of unmapped offline history for an online identity.
* Older providers retain positive evidence but cannot certify the wider scope.
*/
default PlayerPresence checkOnlineHistory(UUID identity) throws Exception {
PlayerPresence evidence = check(identity);
return evidence == PlayerPresence.NEVER_JOINED ? PlayerPresence.UNKNOWN : evidence;
}
}
Loading
Loading