Skip to content

Instantly share code, notes, and snippets.

@Braayy
Created April 14, 2020 04:07
Show Gist options
  • Save Braayy/98c1943b5bb30e39b6132806a4a67394 to your computer and use it in GitHub Desktop.
Save Braayy/98c1943b5bb30e39b6132806a4a67394 to your computer and use it in GitHub Desktop.
A simple and lightweight but powerful one class scoreboard lib
package braayy.simplescoreboard;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Player;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
/*
Copyright 2020 Braayy(github.com/Braayy)
You can use and edit as much as you want since you keep this text on the source code.
*/
@SuppressWarnings({"unchecked", "unused"})
public class SimpleScoreboard {
// region reflection cache
private static Class<?> PACKET_PLAY_OUT_SCOREBOARD_OBJETIVE_CLASS;
private static Class<?> PACKET_PLAY_OUT_SCOREBOARD_TEAM_CLASS;
private static Class<?> PACKET_PLAY_OUT_SCOREBOARD_SCORE_CLASS;
private static Class<?> PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJETIVE_CLASS;
private static Object INTEGER;
private static Object CHANGE;
private static Method GET_HANDLE_METHOD;
private static Method SEND_PACKET_METHOD;
private static Field PLAYER_CONNECTION_FIELD;
static {
try {
PACKET_PLAY_OUT_SCOREBOARD_OBJETIVE_CLASS = getNMSClass("PacketPlayOutScoreboardObjective");
PACKET_PLAY_OUT_SCOREBOARD_TEAM_CLASS = getNMSClass("PacketPlayOutScoreboardTeam");
PACKET_PLAY_OUT_SCOREBOARD_SCORE_CLASS = getNMSClass("PacketPlayOutScoreboardScore");
PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJETIVE_CLASS = getNMSClass("PacketPlayOutScoreboardDisplayObjective");
INTEGER = getNMSClass("IScoreboardCriteria").getClasses()[0].getDeclaredField("INTEGER").get(null);
CHANGE = getNMSClass("PacketPlayOutScoreboardScore").getClasses()[0].getDeclaredField("CHANGE").get(null);
GET_HANDLE_METHOD = Class.forName("org.bukkit.craftbukkit." + getNMSVersion() + ".entity.CraftPlayer").getDeclaredMethod("getHandle");
SEND_PACKET_METHOD = getNMSClass("PlayerConnection").getDeclaredMethod("sendPacket", getNMSClass("Packet"));
PLAYER_CONNECTION_FIELD = getNMSClass("EntityPlayer").getDeclaredField("playerConnection");
} catch (Exception exception) {
exception.printStackTrace();
}
}
// endregion
/**
* The title of the Scoreboard.
*/
private final String title;
/**
* The lines of the Scoreboard.
*/
private final String[] lines;
public SimpleScoreboard(String title) {
this.title = title;
this.lines = new String[21];
}
/**
* Sets the line number {line} to {text}.
* @param line The line number to be set.
* @param text The text to the line number.
*/
public void setLine(int line, String text) {
line = Math.abs(line);
if (line > 21) {
throw new IllegalArgumentException("This lib only supports 21 lines of scoreboard.");
}
if (text.length() > 32) {
throw new IllegalArgumentException("This lib only supports 32 characters in a line.");
}
this.lines[line] = text;
}
/**
* Gets the text of a line number.
* @param line The line number to be get.
* @return The text of that line number.
*/
public String getLine(int line) {
line = Math.abs(line);
if (line > 21) {
throw new IllegalArgumentException("This lib only supports 21 lines of scoreboard.");
}
return this.lines[line];
}
/**
* Shows the Scoreboard to the {player}.
* @param player The player who the Scoreboard will be show to.
*/
public void show(Player player) {
try {
final Object packObjective = createObjectivePacket();
sendPacket(player, packObjective);
for (int lineNumber = 0; lineNumber < this.lines.length; lineNumber++) {
final String text = this.lines[lineNumber];
if (text != null) {
final String playerName = ChatColor.values()[lineNumber].toString() + ChatColor.RESET;
final String teamName = "l" + lineNumber;
final Object packTeam = createTeamPacket(teamName, text, playerName, true);
sendPacket(player, packTeam);
final Object packScore = createScorePacket(playerName, lineNumber);
sendPacket(player, packScore);
}
}
final Object packDisplay = createDisplayPacket();
sendPacket(player, packDisplay);
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* Updates the Scoreboard with the new text of the {lines}.
* @param player The player who the Scoreboard will be updated to.
*/
public void update(Player player) {
try {
for (int lineNumber = 0; lineNumber < this.lines.length; lineNumber++) {
final String line = this.lines[lineNumber];
if (line != null) {
final String teamName = "l" + lineNumber;
final Object packTeam = createTeamPacket(teamName, line, null, false);
sendPacket(player, packTeam);
}
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* Creates the objective packet with the {this.title} title.
* @return The objective packet
* @throws Exception If something goes wrong.
*/
private Object createObjectivePacket() throws Exception {
final Object packObjective = PACKET_PLAY_OUT_SCOREBOARD_OBJETIVE_CLASS.newInstance();
setFieldValue(packObjective, "a", this.title);
setFieldValue(packObjective, "b", this.title);
setFieldValue(packObjective, "c", INTEGER);
setFieldValue(packObjective, "d", 0);
return packObjective;
}
/**
* Creates the team packet with that {text} as a prefix-suffix mix.
* @param teamName The team name to be created.
* @param text The text to be displayed.
* @param playerName The player name associated with that team.
* @param addEntry If is should add the player name to the list.
* @return The team packet
* @throws Exception If something goes wrong.
*/
private Object createTeamPacket(String teamName, String text, String playerName, boolean addEntry) throws Exception {
final Object packTeam = PACKET_PLAY_OUT_SCOREBOARD_TEAM_CLASS.newInstance();
setFieldValue(packTeam, "a", teamName);
setFieldValue(packTeam, "h", 0);
setFieldValue(packTeam, "b", teamName);
if (text.length() > 16) {
setFieldValue(packTeam, "c", text.substring(0, 16));
setFieldValue(packTeam, "d", text.substring(16));
} else {
setFieldValue(packTeam, "c", "");
setFieldValue(packTeam, "d", text);
}
setFieldValue(packTeam, "i", 0);
setFieldValue(packTeam, "e", "always");
setFieldValue(packTeam, "f", -1);
if (addEntry) {
final Field field = packTeam.getClass().getDeclaredField("g");
field.setAccessible(true);
List<String> playerList = (List<String>) field.get(packTeam);
playerList.add(playerName);
}
return packTeam;
}
/**
* Creates the score packet with the player name associated with the team on the line number.
* @param playerName The player name associated with the team.
* @param lineNumber The line number where it should be displayed.
* @return The score packet
* @throws Exception If something goes wrong.
*/
private Object createScorePacket(String playerName, int lineNumber) throws Exception {
final Object packScore = PACKET_PLAY_OUT_SCOREBOARD_SCORE_CLASS.newInstance();
setFieldValue(packScore, "a", playerName);
setFieldValue(packScore, "b", this.title);
setFieldValue(packScore, "c", lineNumber);
setFieldValue(packScore, "d", CHANGE);
return packScore;
}
/**
* Creates the display packet, it just display all that content above.
* @return The display packet
* @throws Exception If something goes wrong.
*/
private Object createDisplayPacket() throws Exception {
final Object packDisplay = PACKET_PLAY_OUT_SCOREBOARD_DISPLAY_OBJETIVE_CLASS.newInstance();
setFieldValue(packDisplay, "a", 1);
setFieldValue(packDisplay, "b", this.title);
return packDisplay;
}
/**
* Sends a packet to a player.
* @param player The player that the packet will be sent.
* @param packet The packet that will be sent.
* @throws Exception If something goes wrong.
*/
private static void sendPacket(Player player, Object packet) throws Exception {
final Object handle = GET_HANDLE_METHOD.invoke(player);
final Object playerConnection = PLAYER_CONNECTION_FIELD.get(handle);
SEND_PACKET_METHOD.invoke(playerConnection, packet);
}
/**
* @param className The NMS Class name.
* @return The NMS Class associated with that {className}.
* @throws Exception If it doesn't find that class.
*/
private static Class<?> getNMSClass(String className) throws Exception{
return Class.forName("net.minecraft.server." + getNMSVersion() + "." + className);
}
/**
* @return The NMS version that the server is running at.
*/
private static String getNMSVersion() {
return Bukkit.getServer().getClass().getName().split("\\.")[3];
}
/**
* Sets the value of a field in the {obj} object to {fieldValue}
* @param obj The object to be modified.
* @param fieldName The field name.
* @param fieldValue The field value.
* @throws Exception If something goes wrong.
*/
private static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception {
final Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, fieldValue);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment