4343 * The project is on <a href="https://github.com/MrMicky-FR/FastBoard">GitHub</a>.
4444 *
4545 * @author MrMicky
46- * @version 2.0.2
46+ * @version 2.1.0
4747 */
4848public abstract class FastBoardBase <T > {
4949
@@ -59,6 +59,7 @@ public abstract class FastBoardBase<T> {
5959 private static final MethodHandle PLAYER_CONNECTION ;
6060 private static final MethodHandle SEND_PACKET ;
6161 private static final MethodHandle PLAYER_GET_HANDLE ;
62+ private static final MethodHandle FIXED_NUMBER_FORMAT ;
6263 // Scoreboard packets
6364 private static final FastReflection .PacketConstructor PACKET_SB_OBJ ;
6465 private static final FastReflection .PacketConstructor PACKET_SB_DISPLAY_OBJ ;
@@ -125,14 +126,18 @@ public abstract class FastBoardBase<T> {
125126 Optional <Class <?>> numberFormat = FastReflection .nmsOptionalClass ("network.chat.numbers" , "NumberFormat" );
126127 MethodHandle packetSbSetScore ;
127128 MethodHandle packetSbResetScore = null ;
129+ MethodHandle fixedFormatConstructor = null ;
128130 Object blankNumberFormat = null ;
129131
130132 if (numberFormat .isPresent ()) { // 1.20.3
131133 Class <?> blankFormatClass = FastReflection .nmsClass ("network.chat.numbers" , "BlankFormat" );
134+ Class <?> fixedFormatClass = FastReflection .nmsClass ("network.chat.numbers" , "FixedFormat" );
132135 Class <?> resetScoreClass = FastReflection .nmsClass (gameProtocolPackage , "ClientboundResetScorePacket" );
133136 MethodType setScoreType = MethodType .methodType (void .class , String .class , String .class , int .class , CHAT_COMPONENT_CLASS , numberFormat .get ());
134137 MethodType removeScoreType = MethodType .methodType (void .class , String .class , String .class );
138+ MethodType fixedFormatType = MethodType .methodType (void .class , CHAT_COMPONENT_CLASS );
135139 Optional <Field > blankField = Arrays .stream (blankFormatClass .getFields ()).filter (f -> f .getType () == blankFormatClass ).findAny ();
140+ fixedFormatConstructor = lookup .findConstructor (fixedFormatClass , fixedFormatType );
136141 packetSbSetScore = lookup .findConstructor (packetSbScoreClass , setScoreType );
137142 packetSbResetScore = lookup .findConstructor (resetScoreClass , removeScoreType );
138143 blankNumberFormat = blankField .isPresent () ? blankField .get ().get (null ) : null ;
@@ -148,6 +153,7 @@ public abstract class FastBoardBase<T> {
148153 PACKET_SB_RESET_SCORE = packetSbResetScore ;
149154 PACKET_SB_TEAM = FastReflection .findPacketConstructor (packetSbTeamClass , lookup );
150155 PACKET_SB_SERIALIZABLE_TEAM = sbTeamClass == null ? null : FastReflection .findPacketConstructor (sbTeamClass , lookup );
156+ FIXED_NUMBER_FORMAT = fixedFormatConstructor ;
151157 BLANK_NUMBER_FORMAT = blankNumberFormat ;
152158
153159 for (Class <?> clazz : Arrays .asList (packetSbObjClass , packetSbDisplayObjClass , packetSbScoreClass , packetSbTeamClass , sbTeamClass )) {
@@ -188,6 +194,7 @@ public abstract class FastBoardBase<T> {
188194 private final String id ;
189195
190196 private final List <T > lines = new ArrayList <>();
197+ private final List <T > scores = new ArrayList <>();
191198 private T title = emptyLine ();
192199
193200 private boolean deleted = false ;
@@ -261,6 +268,19 @@ public T getLine(int line) {
261268 return this .lines .get (line );
262269 }
263270
271+ /**
272+ * Get how a specific line's score is displayed. On 1.20.2 or below, the value returned isn't used.
273+ *
274+ * @param line the line number
275+ * @return the text of how the line is displayed
276+ * @throws IndexOutOfBoundsException if the line is higher than {@code size}
277+ */
278+ public Optional <T > getScore (int line ) {
279+ checkLineNumber (line , true , false );
280+
281+ return Optional .ofNullable (this .scores .get (line ));
282+ }
283+
264284 /**
265285 * Update a single scoreboard line.
266286 *
@@ -269,27 +289,49 @@ public T getLine(int line) {
269289 * @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1}
270290 */
271291 public synchronized void updateLine (int line , T text ) {
272- checkLineNumber (line , false , true );
292+ updateLine (line , text , null );
293+ }
294+
295+ /**
296+ * Update a single scoreboard line including how its score is displayed.
297+ * The score will only be displayed on 1.20.3 and higher.
298+ *
299+ * @param line the line number
300+ * @param text the new line text
301+ * @param scoreText the new line's score, if null will not change current value
302+ * @throws IndexOutOfBoundsException if the line is higher than {@link #size() size() + 1}
303+ */
304+ public synchronized void updateLine (int line , T text , T scoreText ) {
305+ checkLineNumber (line , false , false );
273306
274307 try {
275308 if (line < size ()) {
276309 this .lines .set (line , text );
310+ this .scores .set (line , scoreText );
277311
278312 sendLineChange (getScoreByLine (line ));
313+
314+ if (customScoresSupported ()) {
315+ sendScorePacket (getScoreByLine (line ), ScoreboardAction .CHANGE );
316+ }
317+
279318 return ;
280319 }
281320
282321 List <T > newLines = new ArrayList <>(this .lines );
322+ List <T > newScores = new ArrayList <>(this .scores );
283323
284324 if (line > size ()) {
285325 for (int i = size (); i < line ; i ++) {
286326 newLines .add (emptyLine ());
327+ newScores .add (null );
287328 }
288329 }
289330
290331 newLines .add (text );
332+ newScores .add (scoreText );
291333
292- updateLines (newLines );
334+ updateLines (newLines , newScores );
293335 } catch (Throwable t ) {
294336 throw new RuntimeException ("Unable to update scoreboard lines" , t );
295337 }
@@ -308,8 +350,10 @@ public synchronized void removeLine(int line) {
308350 }
309351
310352 List <T > newLines = new ArrayList <>(this .lines );
353+ List <T > newScores = new ArrayList <>(this .scores );
311354 newLines .remove (line );
312- updateLines (newLines );
355+ newScores .remove (line );
356+ updateLines (newLines , newScores );
313357 }
314358
315359 /**
@@ -331,13 +375,35 @@ public void updateLines(T... lines) {
331375 * @throws IllegalStateException if {@link #delete()} was call before
332376 */
333377 public synchronized void updateLines (Collection <T > lines ) {
378+ updateLines (lines , null );
379+ }
380+
381+ /**
382+ * Update the lines and how their score is displayed on the scoreboard.
383+ * The scores will only be displayed for servers on 1.20.3 and higher.
384+ *
385+ * @param lines the new scoreboard lines
386+ * @param scores the set for how each line's score should be, if null will fall back to default (blank)
387+ * @throws IllegalArgumentException if one line is longer than 30 chars on 1.12 or lower
388+ * @throws IllegalArgumentException if lines and scores are not the same size
389+ * @throws IllegalStateException if {@link #delete()} was call before
390+ */
391+ public synchronized void updateLines (Collection <T > lines , Collection <T > scores ) {
334392 Objects .requireNonNull (lines , "lines" );
335393 checkLineNumber (lines .size (), false , true );
336394
395+ if (scores != null && scores .size () != lines .size ()) {
396+ throw new IllegalArgumentException ("The size of the scores must match the size of the board" );
397+ }
398+
337399 List <T > oldLines = new ArrayList <>(this .lines );
338400 this .lines .clear ();
339401 this .lines .addAll (lines );
340402
403+ List <T > oldScores = new ArrayList <>(this .scores );
404+ this .scores .clear ();
405+ this .scores .addAll (scores != null ? scores : Collections .nCopies (lines .size (), null ));
406+
341407 int linesSize = this .lines .size ();
342408
343409 try {
@@ -348,7 +414,6 @@ public synchronized void updateLines(Collection<T> lines) {
348414 for (int i = oldLinesCopy .size (); i > linesSize ; i --) {
349415 sendTeamPacket (i - 1 , TeamMode .REMOVE );
350416 sendScorePacket (i - 1 , ScoreboardAction .REMOVE );
351-
352417 oldLines .remove (0 );
353418 }
354419 } else {
@@ -363,12 +428,94 @@ public synchronized void updateLines(Collection<T> lines) {
363428 if (!Objects .equals (getLineByScore (oldLines , i ), getLineByScore (i ))) {
364429 sendLineChange (i );
365430 }
431+ if (!Objects .equals (getLineByScore (oldScores , i ), getLineByScore (this .scores , i ))) {
432+ sendScorePacket (i , ScoreboardAction .CHANGE );
433+ }
366434 }
367435 } catch (Throwable t ) {
368436 throw new RuntimeException ("Unable to update scoreboard lines" , t );
369437 }
370438 }
371439
440+ /**
441+ * Update how a specified line's score is displayed on the scoreboard. A null value will reset the displayed
442+ * text back to default. The scores will only be displayed for servers on 1.20.3 and higher.
443+ *
444+ * @param line the line number
445+ * @param text the text to be displayed as the score. if null, no score will be displayed
446+ * @throws IllegalArgumentException if the line number is not in range
447+ * @throws IllegalStateException if {@link #delete()} was call before
448+ */
449+ public synchronized void updateScore (int line , T text ) {
450+ checkLineNumber (line , true , false );
451+
452+ this .scores .set (line , text );
453+
454+ try {
455+ if (customScoresSupported ()) {
456+ sendScorePacket (getScoreByLine (line ), ScoreboardAction .CHANGE );
457+ }
458+ } catch (Throwable e ) {
459+ throw new RuntimeException ("Unable to update line score" , e );
460+ }
461+ }
462+
463+ /**
464+ * Reset a line's score back to default (blank). The score will only be displayed for servers on 1.20.3 and higher.
465+ *
466+ * @param line the line number
467+ * @throws IllegalArgumentException if the line number is not in range
468+ * @throws IllegalStateException if {@link #delete()} was call before
469+ */
470+ public synchronized void removeScore (int line ) {
471+ updateScore (line , null );
472+ }
473+
474+ /**
475+ * Update how all lines' scores are displayed. A value of null will reset the displayed text back to default.
476+ * The scores will only be displayed for servers on 1.20.3 and higher.
477+ *
478+ * @param texts the set of texts to be displayed as the scores
479+ * @throws IllegalArgumentException if the size of the texts does not match the current size of the board
480+ * @throws IllegalStateException if {@link #delete()} was call before
481+ */
482+ public synchronized void updateScores (T ... texts ) {
483+ updateScores (Arrays .asList (texts ));
484+ }
485+
486+ /**
487+ * Update how all lines' scores are displayed. A null value will reset the displayed
488+ * text back to default (blank). Only available on 1.20.3+ servers.
489+ *
490+ * @param texts the set of texts to be displayed as the scores
491+ * @throws IllegalArgumentException if the size of the texts does not match the current size of the board
492+ * @throws IllegalStateException if {@link #delete()} was call before
493+ */
494+ public synchronized void updateScores (Collection <T > texts ) {
495+ Objects .requireNonNull (texts , "texts" );
496+
497+ if (this .scores .size () != this .lines .size ()) {
498+ throw new IllegalArgumentException ("The size of the scores must match the size of the board" );
499+ }
500+
501+ List <T > newScores = new ArrayList <>(texts );
502+ for (int i = 0 ; i < this .scores .size (); i ++) {
503+ if (Objects .equals (this .scores .get (i ), newScores .get (i ))) {
504+ continue ;
505+ }
506+
507+ this .scores .set (i , newScores .get (i ));
508+
509+ try {
510+ if (customScoresSupported ()) {
511+ sendScorePacket (getScoreByLine (i ), ScoreboardAction .CHANGE );
512+ }
513+ } catch (Throwable e ) {
514+ throw new RuntimeException ("Unable to update scores" , e );
515+ }
516+ }
517+ }
518+
372519 /**
373520 * Get the player who has the scoreboard.
374521 *
@@ -396,6 +543,15 @@ public boolean isDeleted() {
396543 return this .deleted ;
397544 }
398545
546+ /**
547+ * Get if the server supports custom scoreboard scores (1.20.3+ servers only).
548+ *
549+ * @return true if the server supports custom scores
550+ */
551+ public boolean customScoresSupported () {
552+ return BLANK_NUMBER_FORMAT != null ;
553+ }
554+
399555 /**
400556 * Get the scoreboard size (the number of lines).
401557 *
@@ -528,7 +684,12 @@ private void sendModernScorePacket(int score, ScoreboardAction action) throws Th
528684 return ;
529685 }
530686
531- sendPacket (PACKET_SB_SET_SCORE .invoke (objName , this .id , score , null , BLANK_NUMBER_FORMAT ));
687+ T scoreFormat = getLineByScore (this .scores , score );
688+ Object format = scoreFormat != null
689+ ? FIXED_NUMBER_FORMAT .invoke (toMinecraftComponent (scoreFormat ))
690+ : BLANK_NUMBER_FORMAT ;
691+
692+ sendPacket (PACKET_SB_SET_SCORE .invoke (objName , this .id , score , null , format ));
532693 }
533694
534695 protected void sendTeamPacket (int score , TeamMode mode ) throws Throwable {
0 commit comments