2525import java .util .concurrent .ConcurrentHashMap ;
2626import java .util .concurrent .Executors ;
2727import java .util .concurrent .ScheduledExecutorService ;
28+ import java .lang .reflect .Method ;
2829import java .util .concurrent .atomic .AtomicInteger ;
2930import java .util .function .Function ;
3031
31- import de .tr7zw .changeme .nbtapi .NBT ;
32- import de .tr7zw .changeme .nbtapi .iface .ReadableItemNBT ;
32+ import org .bukkit .plugin .Plugin ;
3333import net .kyori .adventure .text .Component ;
34- import net .kyori .adventure .text .serializer .plain .PlainTextComponentSerializer ;
34+ import net .kyori .adventure .text .KeybindComponent ;
35+ import net .kyori .adventure .text .ScoreComponent ;
36+ import net .kyori .adventure .text .SelectorComponent ;
37+ import net .kyori .adventure .text .TextComponent ;
38+ import net .kyori .adventure .text .TranslatableComponent ;
3539import org .bukkit .NamespacedKey ;
3640import org .bukkit .inventory .ItemFlag ;
3741import org .bukkit .persistence .PersistentDataContainer ;
@@ -46,6 +50,12 @@ public final class PlayerInventoryBroadcaster
4650{
4751 private static final PlayerInventoryBroadcaster INSTANCE = new PlayerInventoryBroadcaster ();
4852 private static final long REFRESH_TICKS = 20L ; // 1 second
53+ private static final int MAX_NAME_CHARS = 256 ;
54+ private static final int MAX_LORE_LINES = 20 ;
55+ private static final int MAX_LORE_LINE_CHARS = 256 ;
56+ private static final int MAX_NBT_CHARS = 4096 ;
57+ private static final int MAX_PDC_KEYS = 64 ;
58+ private static final int MAX_PDC_KEY_CHARS = 128 ;
4959
5060 public static PlayerInventoryBroadcaster get ()
5161 {
@@ -87,7 +97,7 @@ public synchronized void start()
8797
8898 try
8999 {
90- NBT . preloadApi ();
100+ NbtApiBridge . preload ();
91101 }
92102 catch (Throwable t )
93103 {
@@ -246,6 +256,67 @@ private String buildPayload(Player p)
246256 return new GsonBuilder ().serializeNulls ().create ().toJson (root );
247257 }
248258
259+ private static String limit (String value , int maxChars )
260+ {
261+ if (value == null || value .length () <= maxChars ) return value ;
262+ return value .substring (0 , maxChars ) + "… [Truncated " + (value .length () - maxChars ) + " characters]" ;
263+ }
264+
265+ private static void putLimited (Map <String , Object > map , String key , String value , int maxChars )
266+ {
267+ if (value == null || value .isEmpty ()) return ;
268+ map .put (key , limit (value , maxChars ));
269+ if (value .length () > maxChars )
270+ {
271+ map .put (key + "Truncated" , true );
272+ map .put (key + "TruncatedChars" , value .length () - maxChars );
273+ }
274+ }
275+
276+ private static void putLimited (Map <String , Object > map , String key , Component component , int maxChars )
277+ {
278+ LimitedText text = limitedPlainText (component , maxChars );
279+ if (text .text ().isEmpty ()) return ;
280+ map .put (key , text .truncated ()
281+ ? text .text () + "… [Truncated " + (text .totalChars () - maxChars ) + " characters]"
282+ : text .text ());
283+ if (text .truncated ())
284+ {
285+ map .put (key + "Truncated" , true );
286+ map .put (key + "TruncatedChars" , text .totalChars () - maxChars );
287+ }
288+ }
289+
290+ private static LimitedText limitedPlainText (Component component , int maxChars )
291+ {
292+ StringBuilder out = new StringBuilder (Math .min (maxChars , 256 ));
293+ int total = appendPlain (component , out , maxChars );
294+ return new LimitedText (out .toString (), total , total > maxChars );
295+ }
296+
297+ private static int appendPlain (Component component , StringBuilder out , int maxChars )
298+ {
299+ int total = appendComponentValue (component , out , maxChars );
300+ for (Component child : component .children ())
301+ {
302+ total += appendPlain (child , out , maxChars - Math .min (out .length (), maxChars ));
303+ }
304+ return total ;
305+ }
306+
307+ private static int appendComponentValue (Component component , StringBuilder out , int remaining )
308+ {
309+ String value = null ;
310+ if (component instanceof TextComponent text ) value = text .content ();
311+ else if (component instanceof TranslatableComponent translatable ) value = translatable .fallback () != null ? translatable .fallback () : translatable .key ();
312+ else if (component instanceof KeybindComponent keybind ) value = keybind .keybind ();
313+ else if (component instanceof ScoreComponent score ) value = score .value () != null ? score .value () : score .name ();
314+ else if (component instanceof SelectorComponent selector ) value = selector .pattern ();
315+ if (value == null || value .isEmpty ()) return 0 ;
316+ if (remaining > 0 ) out .append (value , 0 , Math .min (value .length (), remaining ));
317+ return value .length ();
318+ }
319+
249320 private static Map <String , Object > serializeItem (ItemStack item )
250321 {
251322 if (item == null || item .getType ().isAir ()) return null ;
@@ -274,20 +345,27 @@ private static Map<String, Object> serializeItem(ItemStack item)
274345 try
275346 {
276347 Component name = meta .displayName ();
277- if (name != null ) m . put ( "name" , PlainTextComponentSerializer . plainText (). serialize ( name ) );
348+ if (name != null ) putLimited ( m , "name" , name , MAX_NAME_CHARS );
278349 }
279350 catch (Throwable ignored ) {}
280351 try
281352 {
282353 List <Component > lore = meta .lore ();
283354 if (lore != null && !lore .isEmpty ())
284355 {
285- List <String > out = new ArrayList <>(lore .size ());
286- for (Component c : lore )
356+ int count = Math .min (lore .size (), MAX_LORE_LINES );
357+ List <String > out = new ArrayList <>(count );
358+ boolean truncated = lore .size () > MAX_LORE_LINES ;
359+ for (int i = 0 ; i < count ; i ++)
287360 {
288- out .add (PlainTextComponentSerializer .plainText ().serialize (c ));
361+ LimitedText line = limitedPlainText (lore .get (i ), MAX_LORE_LINE_CHARS );
362+ if (line .truncated ()) truncated = true ;
363+ out .add (line .truncated ()
364+ ? line .text () + "… [Truncated " + (line .totalChars () - MAX_LORE_LINE_CHARS ) + " characters]"
365+ : line .text ());
289366 }
290367 m .put ("lore" , out );
368+ if (truncated ) m .put ("loreTruncated" , true );
291369 }
292370 }
293371 catch (Throwable ignored ) {}
@@ -328,26 +406,77 @@ private static Map<String, Object> serializeItem(ItemStack item)
328406 if (!keys .isEmpty ())
329407 {
330408 Set <String > out = new TreeSet <>();
331- for (NamespacedKey k : keys ) out .add (k .toString ());
409+ boolean truncated = keys .size () > MAX_PDC_KEYS ;
410+ int count = 0 ;
411+ for (NamespacedKey k : keys )
412+ {
413+ if (count ++ >= MAX_PDC_KEYS ) break ;
414+ String key = k .toString ();
415+ if (key .length () > MAX_PDC_KEY_CHARS ) truncated = true ;
416+ out .add (limit (key , MAX_PDC_KEY_CHARS ));
417+ }
332418 m .put ("pdcKeys" , out );
419+ if (truncated ) m .put ("pdcKeysTruncated" , true );
333420 }
334421 }
335422 catch (Throwable ignored ) {}
336423
337424 try
338425 {
339- Function <ReadableItemNBT , String > toSnbt = ReadableItemNBT ::toString ;
340- String snbt = NBT .get (item , toSnbt );
426+ String snbt = NbtApiBridge .toSnbt (item );
341427 if (snbt != null && !snbt .isEmpty () && !"{}" .equals (snbt ))
342428 {
343- m . put ( "nbt" , snbt );
429+ putLimited ( m , "nbt" , snbt , MAX_NBT_CHARS );
344430 }
345431 }
346432 catch (Throwable ignored ) {}
347433 }
348434 return m ;
349435 }
350436
437+ private record LimitedText (String text , int totalChars , boolean truncated ) {}
438+
439+ private static final class NbtApiBridge
440+ {
441+ private static volatile Method getMethod ;
442+ private static volatile Method preloadMethod ;
443+ static void preload () throws Exception
444+ {
445+ Method method = preloadMethod ;
446+ if (method == null )
447+ {
448+ Class <?> nbt = nbtClass ();
449+ method = nbt .getMethod ("preloadApi" );
450+ preloadMethod = method ;
451+ }
452+ method .invoke (null );
453+ }
454+
455+ static String toSnbt (ItemStack item ) throws Exception
456+ {
457+ Method method = getMethod ;
458+ if (method == null )
459+ {
460+ Class <?> nbt = nbtClass ();
461+ method = nbt .getMethod ("get" , ItemStack .class , Function .class );
462+ getMethod = method ;
463+ }
464+ Function <Object , String > stringify = Object ::toString ;
465+ Object result = method .invoke (null , item , stringify );
466+ return result instanceof String s ? s : null ;
467+ }
468+
469+ private static Class <?> nbtClass () throws ClassNotFoundException
470+ {
471+ Plugin plugin = Bukkit .getPluginManager ().getPlugin ("NBTAPI" );
472+ if (plugin == null || !plugin .isEnabled ())
473+ {
474+ throw new ClassNotFoundException ("NBTAPI plugin is not enabled" );
475+ }
476+ return Class .forName ("de.tr7zw.changeme.nbtapi.NBT" , true , plugin .getClass ().getClassLoader ());
477+ }
478+ }
479+
351480 private static final class Subscriber
352481 {
353482 final AsyncContext ctx ;
0 commit comments