diff --git a/build.gradle.kts b/build.gradle.kts index 4fe67b7..6112fe7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,13 +29,12 @@ dependencies { implementation("org.projectlombok:lombok:1.18.46") annotationProcessor("org.projectlombok:lombok:1.18.46") compileOnly("io.papermc.paper:paper-api:26.1.2.build.+") - implementation("dev.plex:server:1.7-SNAPSHOT") + implementation("dev.plex:server:2.0-SNAPSHOT") implementation("org.json:json:20251224") implementation("org.reflections:reflections:0.10.2") plexLibrary("org.eclipse.jetty:jetty-server:12.1.9") plexLibrary("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.1.9") plexLibrary("org.eclipse.jetty:jetty-proxy:12.1.9") - implementation("de.tr7zw:item-nbt-api:2.15.7") implementation(platform("com.intellectualsites.bom:bom-newest:1.56")) // Ref: https://github.com/IntellectualSites/bom compileOnly("com.fastasyncworldedit:FastAsyncWorldEdit-Core") implementation("commons-io:commons-io:2.22.0") diff --git a/src/main/java/dev/plex/request/impl/PlayerInventoryBroadcaster.java b/src/main/java/dev/plex/request/impl/PlayerInventoryBroadcaster.java index 8cf8d06..bf6532f 100644 --- a/src/main/java/dev/plex/request/impl/PlayerInventoryBroadcaster.java +++ b/src/main/java/dev/plex/request/impl/PlayerInventoryBroadcaster.java @@ -25,13 +25,17 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; -import de.tr7zw.changeme.nbtapi.NBT; -import de.tr7zw.changeme.nbtapi.iface.ReadableItemNBT; +import org.bukkit.plugin.Plugin; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import net.kyori.adventure.text.KeybindComponent; +import net.kyori.adventure.text.ScoreComponent; +import net.kyori.adventure.text.SelectorComponent; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.TranslatableComponent; import org.bukkit.NamespacedKey; import org.bukkit.inventory.ItemFlag; import org.bukkit.persistence.PersistentDataContainer; @@ -46,6 +50,12 @@ public final class PlayerInventoryBroadcaster { private static final PlayerInventoryBroadcaster INSTANCE = new PlayerInventoryBroadcaster(); private static final long REFRESH_TICKS = 20L; // 1 second + private static final int MAX_NAME_CHARS = 256; + private static final int MAX_LORE_LINES = 20; + private static final int MAX_LORE_LINE_CHARS = 256; + private static final int MAX_NBT_CHARS = 4096; + private static final int MAX_PDC_KEYS = 64; + private static final int MAX_PDC_KEY_CHARS = 128; public static PlayerInventoryBroadcaster get() { @@ -87,7 +97,7 @@ public final class PlayerInventoryBroadcaster try { - NBT.preloadApi(); + NbtApiBridge.preload(); } catch (Throwable t) { @@ -246,6 +256,67 @@ public final class PlayerInventoryBroadcaster return new GsonBuilder().serializeNulls().create().toJson(root); } + private static String limit(String value, int maxChars) + { + if (value == null || value.length() <= maxChars) return value; + return value.substring(0, maxChars) + "… [Truncated " + (value.length() - maxChars) + " characters]"; + } + + private static void putLimited(Map map, String key, String value, int maxChars) + { + if (value == null || value.isEmpty()) return; + map.put(key, limit(value, maxChars)); + if (value.length() > maxChars) + { + map.put(key + "Truncated", true); + map.put(key + "TruncatedChars", value.length() - maxChars); + } + } + + private static void putLimited(Map map, String key, Component component, int maxChars) + { + LimitedText text = limitedPlainText(component, maxChars); + if (text.text().isEmpty()) return; + map.put(key, text.truncated() + ? text.text() + "… [Truncated " + (text.totalChars() - maxChars) + " characters]" + : text.text()); + if (text.truncated()) + { + map.put(key + "Truncated", true); + map.put(key + "TruncatedChars", text.totalChars() - maxChars); + } + } + + private static LimitedText limitedPlainText(Component component, int maxChars) + { + StringBuilder out = new StringBuilder(Math.min(maxChars, 256)); + int total = appendPlain(component, out, maxChars); + return new LimitedText(out.toString(), total, total > maxChars); + } + + private static int appendPlain(Component component, StringBuilder out, int maxChars) + { + int total = appendComponentValue(component, out, maxChars); + for (Component child : component.children()) + { + total += appendPlain(child, out, maxChars - Math.min(out.length(), maxChars)); + } + return total; + } + + private static int appendComponentValue(Component component, StringBuilder out, int remaining) + { + String value = null; + if (component instanceof TextComponent text) value = text.content(); + else if (component instanceof TranslatableComponent translatable) value = translatable.fallback() != null ? translatable.fallback() : translatable.key(); + else if (component instanceof KeybindComponent keybind) value = keybind.keybind(); + else if (component instanceof ScoreComponent score) value = score.value() != null ? score.value() : score.name(); + else if (component instanceof SelectorComponent selector) value = selector.pattern(); + if (value == null || value.isEmpty()) return 0; + if (remaining > 0) out.append(value, 0, Math.min(value.length(), remaining)); + return value.length(); + } + private static Map serializeItem(ItemStack item) { if (item == null || item.getType().isAir()) return null; @@ -274,7 +345,7 @@ public final class PlayerInventoryBroadcaster try { Component name = meta.displayName(); - if (name != null) m.put("name", PlainTextComponentSerializer.plainText().serialize(name)); + if (name != null) putLimited(m, "name", name, MAX_NAME_CHARS); } catch (Throwable ignored) {} try @@ -282,12 +353,19 @@ public final class PlayerInventoryBroadcaster List lore = meta.lore(); if (lore != null && !lore.isEmpty()) { - List out = new ArrayList<>(lore.size()); - for (Component c : lore) + int count = Math.min(lore.size(), MAX_LORE_LINES); + List out = new ArrayList<>(count); + boolean truncated = lore.size() > MAX_LORE_LINES; + for (int i = 0; i < count; i++) { - out.add(PlainTextComponentSerializer.plainText().serialize(c)); + LimitedText line = limitedPlainText(lore.get(i), MAX_LORE_LINE_CHARS); + if (line.truncated()) truncated = true; + out.add(line.truncated() + ? line.text() + "… [Truncated " + (line.totalChars() - MAX_LORE_LINE_CHARS) + " characters]" + : line.text()); } m.put("lore", out); + if (truncated) m.put("loreTruncated", true); } } catch (Throwable ignored) {} @@ -328,19 +406,27 @@ public final class PlayerInventoryBroadcaster if (!keys.isEmpty()) { Set out = new TreeSet<>(); - for (NamespacedKey k : keys) out.add(k.toString()); + boolean truncated = keys.size() > MAX_PDC_KEYS; + int count = 0; + for (NamespacedKey k : keys) + { + if (count++ >= MAX_PDC_KEYS) break; + String key = k.toString(); + if (key.length() > MAX_PDC_KEY_CHARS) truncated = true; + out.add(limit(key, MAX_PDC_KEY_CHARS)); + } m.put("pdcKeys", out); + if (truncated) m.put("pdcKeysTruncated", true); } } catch (Throwable ignored) {} try { - Function toSnbt = ReadableItemNBT::toString; - String snbt = NBT.get(item, toSnbt); + String snbt = NbtApiBridge.toSnbt(item); if (snbt != null && !snbt.isEmpty() && !"{}".equals(snbt)) { - m.put("nbt", snbt); + putLimited(m, "nbt", snbt, MAX_NBT_CHARS); } } catch (Throwable ignored) {} @@ -348,6 +434,49 @@ public final class PlayerInventoryBroadcaster return m; } + private record LimitedText(String text, int totalChars, boolean truncated) {} + + private static final class NbtApiBridge + { + private static volatile Method getMethod; + private static volatile Method preloadMethod; + static void preload() throws Exception + { + Method method = preloadMethod; + if (method == null) + { + Class nbt = nbtClass(); + method = nbt.getMethod("preloadApi"); + preloadMethod = method; + } + method.invoke(null); + } + + static String toSnbt(ItemStack item) throws Exception + { + Method method = getMethod; + if (method == null) + { + Class nbt = nbtClass(); + method = nbt.getMethod("get", ItemStack.class, Function.class); + getMethod = method; + } + Function stringify = Object::toString; + Object result = method.invoke(null, item, stringify); + return result instanceof String s ? s : null; + } + + private static Class nbtClass() throws ClassNotFoundException + { + Plugin plugin = Bukkit.getPluginManager().getPlugin("NBTAPI"); + if (plugin == null || !plugin.isEnabled()) + { + throw new ClassNotFoundException("NBTAPI plugin is not enabled"); + } + return Class.forName("de.tr7zw.changeme.nbtapi.NBT", true, plugin.getClass().getClassLoader()); + } + } + private static final class Subscriber { final AsyncContext ctx; diff --git a/src/main/resources/httpd/assets/dashboard.js b/src/main/resources/httpd/assets/dashboard.js index 35f16d6..178b7ff 100644 --- a/src/main/resources/httpd/assets/dashboard.js +++ b/src/main/resources/httpd/assets/dashboard.js @@ -55,9 +55,6 @@ document.querySelectorAll(selector).forEach(el => { if (el.textContent !== value) { el.textContent = value; - el.classList.remove('tick'); - void el.offsetWidth; - el.classList.add('tick'); } }); } diff --git a/src/main/resources/httpd/assets/player.js b/src/main/resources/httpd/assets/player.js index b3320a7..9689b4d 100644 --- a/src/main/resources/httpd/assets/player.js +++ b/src/main/resources/httpd/assets/player.js @@ -268,13 +268,17 @@ } const safeType = escapeHtml(item.type); const safeName = item.name ? escapeHtml(item.name) : null; + const nameTruncated = item.nameTruncated && Number.isFinite(Number(item.nameTruncatedChars)) + ? Number(item.nameTruncatedChars) + : null; const lines = []; lines.push(`
${renderItemIcon(item)}
- ${safeName ? `

${safeName}

` : ''} + ${safeName ? `

${safeName}

` : ''} + ${nameTruncated != null ? `

Name truncated by ${nameTruncated.toLocaleString()} characters

` : ''}

${safeType}

Count: ${item.amount}

@@ -284,7 +288,7 @@ lines.push(`

Lore

    - ${item.lore.map(l => `
  • ${escapeHtml(l)}
  • `).join('')} + ${item.lore.map(l => `
  • ${escapeHtml(l)}
  • `).join('')}
`); } @@ -332,6 +336,7 @@
    ${item.pdcKeys.map(k => `
  • ${escapeHtml(k)}
  • `).join('')}
+ ${item.pdcKeysTruncated ? `

Plugin NBT keys truncated

` : ''}
`); } @@ -344,7 +349,8 @@ Copy -
${escapeHtml(item.nbt)}
+
${escapeHtml(item.nbt)}
+ ${item.nbtTruncated && Number.isFinite(Number(item.nbtTruncatedChars)) ? `

NBT truncated by ${Number(item.nbtTruncatedChars).toLocaleString()} characters

` : ''} `); } @@ -375,7 +381,7 @@
${renderInventoryGrid(inv)}
-
+
${renderDetailPanel(getItemBySlotKey(inv, selectedKey))}
diff --git a/src/main/resources/httpd/index.html b/src/main/resources/httpd/index.html index ddd23c1..eaf76e3 100644 --- a/src/main/resources/httpd/index.html +++ b/src/main/resources/httpd/index.html @@ -98,7 +98,7 @@ HOME Uptime -
+
@@ -108,18 +108,18 @@ HOME World
-
+
Worlds
-
+
Chunks
-
+
Entities
-
+
diff --git a/src/main/resources/module.yml b/src/main/resources/module.yml index 745fce7..f38ab0f 100644 --- a/src/main/resources/module.yml +++ b/src/main/resources/module.yml @@ -1,4 +1,5 @@ name: Module-HTTPD version: 1.7 description: HTTPD server for Plex -main: dev.plex.HTTPDModule \ No newline at end of file +main: dev.plex.HTTPDModule +apiCompatibility: 1 \ No newline at end of file