Skip to content

Instantly share code, notes, and snippets.

@WeiiswurstDev
Forked from zyuiop/README.MD
Last active March 12, 2022 21:11
Show Gist options
  • Save WeiiswurstDev/8e9a6fd0f8eaaa57c2c0fc2ed9fcd0fb to your computer and use it in GitHub Desktop.
Save WeiiswurstDev/8e9a6fd0f8eaaa57c2c0fc2ed9fcd0fb to your computer and use it in GitHub Desktop.
A simple tool to manage scoreboards in minecraft (lines up to 48 characters !). This Fork uses ProtocolLib and is therefore compatible with 1.8 - 1,15,2. To use it in your project, you need to use ProtocolLib as a dependency!!
package dev.wwst.scoreboard;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.google.common.collect.Lists;
import org.bukkit.entity.Player;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author zyuiop
* @author Weiiswurst
* I changed the ScoreboardSign to use the ProtocolLib API.
* This means: No java.reflect, and most importantly, cross-version compatibility!!
*
*/
public class ScoreboardSign {
private boolean created = false;
private final VirtualTeam[] lines = new VirtualTeam[15];
private final Player player;
private String objectiveName;
private final ProtocolManager pm;
/**
* Create a scoreboard sign for a given player and using a specifig objective name
* @param player the player viewing the scoreboard sign
* @param objectiveName the name of the scoreboard sign (displayed at the top of the scoreboard)
*/
public ScoreboardSign(final Player player, final String objectiveName) {
this.player = player;
this.objectiveName = objectiveName;
this.pm = ProtocolLibrary.getProtocolManager();
}
private void sendPacket(PacketContainer packet) {
try {
pm.sendServerPacket(player,packet);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* Send the initial creation packets for this scoreboard sign. Must be called at least once.
*/
public void create() {
if (created)
return;
//final PlayerConnection player = getPlayer();
sendPacket(createObjectivePacket(0, objectiveName));
sendPacket(setObjectiveSlot());
int i = 0;
while (i < lines.length)
sendLine(i++);
created = true;
}
/**
* Send the packets to remove this scoreboard sign. A destroyed scoreboard sign must be recreated using {@link ScoreboardSign#create()} in order
* to be used again
*/
public void destroy() {
if (!created)
return;
sendPacket(createObjectivePacket(1, null));
for (final VirtualTeam team : lines)
if (team != null)
sendPacket(team.removeTeam());
created = false;
}
/**
* Change the name of the objective. The name is displayed at the top of the scoreboard.
* @param name the name of the objective, max 32 char
*/
public void setObjectiveName(final String name) {
this.objectiveName = name;
if (created)
sendPacket(createObjectivePacket(2, name));
}
/**
* Change a scoreboard line and send the packets to the player. Can be called async.
* @param line the number of the line (0 <= line < 15)
* @param value the new value for the scoreboard line
*/
public void setLine(final int line, final String value) {
final VirtualTeam team = getOrCreateTeam(line);
final String old = team.getCurrentPlayer();
if(value.equals(old))
return;
if (old != null && created)
sendPacket(removeLine(old));
team.setValue(value);
sendLine(line);
}
/**
* Set all scoreboard lines to the list and send these to the player.
* @param list The list of new scoreboard lines
*/
public void setLines(final Iterable<String> list) {
int i = 0;
for(final String x : list) {
setLine(i,x);
i++;
}
}
/**
* Remove a given scoreboard line
* @param line the line to remove
*/
public void removeLine(final int line) {
final VirtualTeam team = getOrCreateTeam(line);
final String old = team.getCurrentPlayer();
if (old != null && created) {
sendPacket(removeLine(old));
sendPacket(team.removeTeam());
}
lines[line] = null;
}
/**
* Get the current value for a line
* @param line the line
* @return the content of the line
*/
public String getLine(final int line) {
if (line > 14)
return null;
if (line < 0)
return null;
return getOrCreateTeam(line).getValue();
}
/**
* Get the team assigned to a line
* @return the {@link VirtualTeam} used to display this line
*/
public VirtualTeam getTeam(final int line) {
if (line > 14)
return null;
if (line < 0)
return null;
return getOrCreateTeam(line);
}
/*private PlayerConnection getPlayer() {
return ((CraftPlayer) player).getHandle().playerConnection;
}*/
private void sendLine(final int line) {
if (line > 14)
return;
if (line < 0)
return;
if (!created)
return;
int score = (15 - line);
VirtualTeam val = getOrCreateTeam(line);
for (final PacketContainer packet : val.sendLine())
sendPacket(packet);
sendPacket(sendScore(val.getCurrentPlayer(), score));
val.reset();
}
private VirtualTeam getOrCreateTeam(final int line) {
if (lines[line] == null)
lines[line] = new VirtualTeam("__fakeScore" + line);
return lines[line];
}
/*
Factories
*/
private PacketContainer createObjectivePacket(final int mode, final String displayName) {
//PacketPlayOutScoreboardObjective packet = new PacketPlayOutScoreboardObjective();
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_OBJECTIVE, true);
packet2.getStrings().write(0,player.getName());
packet2.getIntegers().write(0,mode);
// Nom de l'objectif
//setField(packet, "a", player.getName());
// Mode
// 0 : créer
// 1 : Supprimer
// 2 : Mettre à jour
//setField(packet, "d", mode);
if (mode == 0 || mode == 2) {
packet2.getStrings().write(1,displayName);
//setField(packet, "b", displayName);
//setField(packet, "c", IScoreboardCriteria.EnumScoreboardHealthDisplay.INTEGER);
}
return packet2;
}
private PacketContainer setObjectiveSlot() {
//PacketPlayOutScoreboardDisplayObjective packet = new PacketPlayOutScoreboardDisplayObjective();
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_DISPLAY_OBJECTIVE);
packet2.getIntegers().write(0,1);
packet2.getStrings().write(0,player.getName());
// Slot
//setField(packet, "a", 1);
//setField(packet, "b", player.getName());
return packet2;
}
private PacketContainer sendScore(final String line,final int score) {
//PacketPlayOutScoreboardScore packet = new PacketPlayOutScoreboardScore(line);
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_SCORE);
packet2.getStrings().write(0,line);
packet2.getStrings().write(1,player.getName());
packet2.getIntegers().write(0,score);
packet2.getScoreboardActions().write(0, EnumWrappers.ScoreboardAction.CHANGE);
//setField(packet, "b", player.getName());
//setField(packet, "c", score);
//setField(packet, "d", PacketPlayOutScoreboardScore.EnumScoreboardAction.CHANGE);
return packet2;
}
private PacketContainer removeLine(String line) {
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_SCORE);
packet2.getStrings().write(0,line);
packet2.getScoreboardActions().write(0, EnumWrappers.ScoreboardAction.REMOVE);
return packet2;
}
/**
* This class is used to manage the content of a line. Advanced users can use it as they want, but they are encouraged to read and understand the
* code before doing so. Use these methods at your own risk.
*/
private static class VirtualTeam {
private final String name;
private String prefix;
private String suffix;
private String currentPlayer;
private String oldPlayer;
private final ProtocolManager pm;
private boolean prefixChanged, suffixChanged, playerChanged = false;
private boolean first = true;
private VirtualTeam(final String name, final String prefix, final String suffix) {
this.name = name;
this.prefix = prefix;
this.suffix = suffix;
this.pm = ProtocolLibrary.getProtocolManager();
}
private VirtualTeam(final String name) {
this(name, "", "");
}
public String getName() {
return name;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(final String prefix) {
if (this.prefix == null || !this.prefix.equals(prefix))
this.prefixChanged = true;
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(final String suffix) {
if (this.suffix == null || !this.suffix.equals(prefix))
this.suffixChanged = true;
this.suffix = suffix;
}
private PacketContainer createPacket(final int mode) {
//PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam();
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_TEAM,true);
packet2.getStrings().write(0,name);
packet2.getStrings().write(1,"");
packet2.getStrings().write(2,prefix);
packet2.getStrings().write(3,suffix);
packet2.getStrings().write(4,"always");
packet2.getIntegers().write(0,0);
packet2.getIntegers().write(1,mode);
packet2.getIntegers().write(2,0);
//setField(packet, "a", name);
//setField(packet, "h", mode);
//setField(packet, "b", "");
//setField(packet, "c", prefix);
//setField(packet, "d", suffix);
//setField(packet, "i", 0);
//setField(packet, "e", "always");
//setField(packet, "f", 0);
return packet2;
}
public PacketContainer createTeam() {
return createPacket(0);
}
public PacketContainer updateTeam() {
return createPacket(2);
}
public PacketContainer removeTeam() {
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_TEAM);
packet2.getStrings().write(0,name);
packet2.getIntegers().write(1,1);
//PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam();
//setField(packet, "a", name);
//setField(packet, "h", 1);
first = true;
return packet2;
}
public void setPlayer(final String name) {
if (this.currentPlayer == null || !this.currentPlayer.equals(name))
this.playerChanged = true;
this.oldPlayer = this.currentPlayer;
this.currentPlayer = name;
}
public Iterable<PacketContainer> sendLine() {
List<PacketContainer> packets = new ArrayList<>();
if (first) {
packets.add(createTeam());
} else if (prefixChanged || suffixChanged) {
packets.add(updateTeam());
}
if (first || playerChanged) {
if (oldPlayer != null) // remove these two lines ?
packets.add(addOrRemovePlayer(4, oldPlayer)); //
packets.add(changePlayer());
}
if (first)
first = false;
return packets;
}
public void reset() {
prefixChanged = false;
suffixChanged = false;
playerChanged = false;
oldPlayer = null;
}
public PacketContainer changePlayer() {
return addOrRemovePlayer(3, currentPlayer);
}
public PacketContainer addOrRemovePlayer(final int mode, final String playerName) {
//PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam();
//setField(packet, "a", name);
//setField(packet, "h", mode);
PacketContainer packet2 = pm.createPacket(PacketType.Play.Server.SCOREBOARD_TEAM);
packet2.getStrings().write(0,name);
packet2.getIntegers().write(1,mode);
Collection<String> playerNames = Lists.newArrayList(playerName);
packet2.getSpecificModifier(Collection.class).write(0,playerNames);
/*try {
Field f = packet.getClass().getDeclaredField("g");
f.setAccessible(true);
((List<String>) f.get(packet)).add(playerName);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}*/
return packet2;
}
public String getCurrentPlayer() {
return currentPlayer;
}
public String getValue() {
return getPrefix() + getCurrentPlayer() + getSuffix();
}
public void setValue(final String value) {
if (value.length() <= 16) {
setPrefix("");
setSuffix("");
setPlayer(value);
} else if (value.length() <= 32) {
setPrefix(value.substring(0, 16));
setPlayer(value.substring(16));
setSuffix("");
} else if (value.length() <= 48) {
setPrefix(value.substring(0, 16));
setPlayer(value.substring(16, 32));
setSuffix(value.substring(32));
} else {
throw new IllegalArgumentException("Too long value ! Max 48 characters, value was " + value.length() + " !");
}
}
}
/*private static void setField(final Object edit, final String fieldName, final Object value) {
try {
final Field field = edit.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(edit, value);
} catch (final NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}*/
}
@Swiftlicious
Copy link

Swiftlicious commented Jun 23, 2020

Is it possible to use this resource just to create scoreboard teams and not an actual sidebar? It seems to get a team reference you need to create a line from the scoreboard but is it possible without doing that or no?

Yes it is, but you need to change the visibility of the VirtualTeam class (line 254) to public to access the inner class. You can then instantiate the class in your own code with your own params (prefix,suffix,etc.) and use it that way.

Please note that some parts of it are not particularly well explained, for example the mode in addOrRemovePlayer(...), so here is a link to the exact packet documentation that will be useful if you want to figure that stuff out yourself.
IMPORTANT: I linked you the version for MC 1.13+, here is a link to the 1.8 - 1.12.0 version of the packet and a link to the 1.12.1 - 1.13 version of that packet.

However you need a good understanding of how scoreboard teams work to use it that way, and I personally never used the class for that purpose so I might not be able to help you with your problems when using it for that purpose. The original author zyuiop isnt involved in Spigot development anymore so he replies rarely on his, original, version of this class so you might not get quick or any support at all from him either. (He did update the description of his gist for my fork of it, so he is active on gh, just saying that there are no guarantees that he will/can help you with that). What I want to say you is just that you might be on your own with this one.

I hope I could help you! @Swiftlicious

thank you so much! it really did help :D
i'm just wondering are you sure this is 1.15.2 supported because i could've sworn that they've updated their "handle.getStrings()" and handle.getIntegers()" methods to WrappedChatComponents instead using "handle.getChatComponents()"?

I haven't tested this yet for 1.15.2 but I did have errors before using a wrapper (until seeing this resource being made) trying to use the old way of Strings.

for example getPrefix() changed from:

	public String getPrefix() {
		return handle.getStrings().read(2);
	}

to:

    public WrappedChatComponent getPrefix() {
        return handle.getChatComponents().read(1);
    }

EDIT: yes I get out of bound errors from that exact area. (around the createPacket area at packet2.getStrings().write(3, suffix); [line 307] likely because 3 is too high for the new way they read values). I'll try updating it myself but if you're able to maybe you can try adding support for that too?
com.comphenix.protocol.reflect.FieldAccessException: Field index out of bounds. (Index: 3, Size: 3) is the exact error.

DOUBLE EDIT: even after fixing the errors to the correct types to read the packets from it does nothing ;-; I created the teams onEnable, give the tags on join (even debugging to see if the tag is the proper one which it is) and there is no tag being added to the player, I can only assume it's because there's no sending of packets unless I have to use some specific packet method instead of PlayerJoinEvent?
The exact way I'm doing it is this:

	        	teamTag = new VirtualTeam(rankName, getRankPrefix(ranks), getRankSuffix(ranks), getRankColor(ranks));
	        	teamTag.addOrRemovePlayer(3, player.getName());
	        	teamTag.updateTeam();

(The getRankColor(ranks) is to refer to the scoreboardTeam.setColor(ChatColor color) which I'm not sure if ProtocolLib has gotten around or not but you need this value to set the player name's actual username's color rather than putting it in the prefix so I added it in the VirtualTeam's parameters)
while on leave I change the 3 to a 4 and onEnable I remove the last two lines and replace them with just
teamTag.createTeam();

The way I'm seeing if it works is by if my nametag above my player model has set the proper prefix and it hasn't.. I've tried everything I can think of ;-;

@WeiiswurstDev
Copy link
Author

The way I'm seeing if it works is by if my nametag above my player model has set the proper prefix and it hasn't.. I've tried everything I can think of ;-;

@Swiftlicious Just to be safe, are you testing with 2 accounts? You cannot see your own prefix above your player model with just one player.

Sadly I don't know if it works for MC 1.15.2 as the only plugin I made that uses it is a minigame for Spigot 1.8.8, and it works flawlessly there.

If you find a fix to make it work for MC 1.15.2, please send it to me and I will update the gist :)

@Swiftlicious
Copy link

Swiftlicious commented Jun 25, 2020

The way I'm seeing if it works is by if my nametag above my player model has set the proper prefix and it hasn't.. I've tried everything I can think of ;-;

@Swiftlicious Just to be safe, are you testing with 2 accounts? You cannot see your own prefix above your player model with just one player.

Sadly I don't know if it works for MC 1.15.2 as the only plugin I made that uses it is a minigame for Spigot 1.8.8, and it works flawlessly there.

If you find a fix to make it work for MC 1.15.2, please send it to me and I will update the gist :)

Yes I was on two accounts, it even says the teams are in fact being created:
https://i.imgur.com/KoldLNx.jpg
These are the proper teams that the two accounts I logged in on should be added (these are broadcasting after the teams have been created so obviously the teams have been successfully made at this point since i'm referencing directly from VirtualTeam's getPrefix(), getSuffix(), getColor() and getName() and it's all correct. I just don't get the fact on why my nametags are still white instead of the proper color..
Is there something I should be doing within the PacketEvent and not just on PlayerJoinEvent?

EDIT: A little bit of a update I was able to get the correct tag on one of my accounts until I logged out on the account without the tag then it went away.. it seems to work if i /reload then rejoin on one account then the opposite account can see it until that account logs out as well but the account with the tag can't see their own tag and if they relog they keep their tag so that means it kinda works? I'm terrible at packets but this is what I heard is the best way to do nametags to get around bukkit scoreboard compatibility breaking. I feel like I'm sending the packet incorrectly..

@Aurelien30000
Copy link

Aurelien30000 commented Jul 10, 2020

@WeiiswurstDev
Copy link
Author

WeiiswurstDev commented Jul 10, 2020 via email

@idimension18
Copy link

idimension18 commented Jul 29, 2020

did works on minecraft 1.15.2 ?
whenever I try to execute my plugin it display :
com.comphenix.protocol.reflect.FieldAccessException: Field index out of bounds. (Index: 1, Size: 1)

EDIT: i forgot to put my code:

public Map<Player, ScoreboardSign> boards = new HashMap<>();

    @Override
    public void onEnable(){
        getServer().getPluginManager().registerEvents(this, this);
    }

    @EventHandler
    public void onJoin(final PlayerJoinEvent event){
        Player player = event.getPlayer();
        ScoreboardSign scoreboard = new ScoreboardSign(player, "TestScore");
        scoreboard.create();
        scoreboard.setLine(0, "TestPlayer");
        boards.put(player, scoreboard);
    }

@ValkyrieHD42
Copy link

ValkyrieHD42 commented Aug 21, 2020

Hi, very great !
Do you have it done for 1.16.1 ?

I have tried it but it comes with that
image

@WeiiswurstDev
Copy link
Author

Hi, very great !
Do you have it done for 1.16.1 ?

I have tried it but it comes with that
image

Hi!
I sadly have no idea how the new packets look like.
Although there is this updated fork: https://gist.github.com/Aurelien30000/7824cd4bcf91ee2eca37c7fa3c4e9bca
It has some changes, you should try it out and maybe it works....?
I'm sorry that this is the only help I can give you.

@Aurelien30000
Copy link

Aurelien30000 commented Aug 27, 2020

It doesn't work :/ I can't figure out any issue or change, weird...

@Aurelien30000
Copy link

Aurelien30000 commented Aug 27, 2020

Oww I found, in 1.8, b field is a string and in 1.15.2+ (maybe also 1.12, 1.13), b field is an IChatBaseComponent... but how to get it with PLib?

@Aurelien30000
Copy link

Aurelien30000 commented Aug 27, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment