Skip to content
Open
28 changes: 24 additions & 4 deletions INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

Latest version: `2.1.9`

## ProtocolLib

If you're going to use the ProtocolLib packet adapter, make sure to add ProtocolLib as a `depend` or `softdepend` in your `plugin.yml`:
```yaml
depend:
- ProtocolLib
```
or
```yaml
softdepend:
- ProtocolLib
```

## Gradle

```kotlin
Expand All @@ -18,22 +31,23 @@ dependencies {
// Add packet adapter implementations you want:
runtimeOnly("net.megavex:scoreboard-library-modern:$scoreboardLibraryVersion") // 1.17+
runtimeOnly("net.megavex:scoreboard-library-modern:$scoreboardLibraryVersion:mojmap") // Mojang mapped variant (only use if you know what you're doing!)
runtimeOnly("net.megavex:scoreboard-library-protocollib:$scoreboardLibraryVersion") // 1.8+
runtimeOnly("net.megavex:scoreboard-library-packetevents:$scoreboardLibraryVersion") // 1.8+
runtimeOnly("net.megavex:scoreboard-library-v1_8_R3:$scoreboardLibraryVersion") // 1.8

// If using the PacketEvents implementation, scoreboard-library expects PacketEvents to be in the classpath.
// If using the PacketEvents adapter, scoreboard-library expects PacketEvents to be loaded in the classpath.
// Follow either of:
// - https://github.com/retrooper/packetevents/wiki/Depending-on-pre%E2%80%90built-PacketEvents
// - https://github.com/retrooper/packetevents/wiki/Shading-PacketEvents
// Example how to load PacketEvents in your plugin:
// https://github.com/retrooper/packetevents-example/blob/24f0c842d47362aef122b794dea29b8fee113fa3/thread-safe-listener/src/main/java/main/Main.java

// If targeting a Minecraft version without native Adventure support, add it as well:
// If targeting a server version without native Adventure support, add it as well:
implementation("net.kyori:adventure-platform-bukkit:4.0.1")
}
```

You will need to shade these dependencies and relocate them with something
You will need to shade these dependencies and relocate them using something
like [Shadow](https://imperceptiblethoughts.com/shadow/).

## Maven
Expand Down Expand Up @@ -67,6 +81,12 @@ like [Shadow](https://imperceptiblethoughts.com/shadow/).
<!-- For a Mojang mapped variant, uncomment line below (only use if you know what you're doing!): -->
<!-- <classifier>mojmap</classifier> -->
</dependency>
<dependency>
<groupId>net.megavex</groupId>
<artifactId>scoreboard-library-protocollib</artifactId>
<version>{VERSION HERE}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.megavex</groupId>
<artifactId>scoreboard-library-packetevents</artifactId>
Expand All @@ -81,7 +101,7 @@ like [Shadow](https://imperceptiblethoughts.com/shadow/).
</dependency>

<!--
If using the PacketEvents implementation, scoreboard-library expects PacketEvents to be in the classpath.
If using the PacketEvents adapter, scoreboard-library expects PacketEvents to be loaded in the classpath.
Follow either of:
- https://github.com/retrooper/packetevents/wiki/Depending-on-pre-built-PacketEvents
- https://github.com/retrooper/packetevents/wiki/Shading-PacketEvents
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Join the [Discord](https://discord.gg/v7nmTDTW8W) or create an issue for support
## Available Packet Adapters

- **modern.** Supports 1.17-1.20.6. Can take advantage of [Paper](https://papermc.io)'s native adventure support to be more efficient.
- **ProtocolLib**. Supports 1.8+. Requires [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) to be installed on the server.
- **PacketEvents.** Supports 1.8+. Requires [PacketEvents 2.0](https://github.com/retrooper/packetevents/tree/2.0) to be shaded or installed as a plugin.
- **1.8.8.**

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ devBundle = "1.21-R0.1-SNAPSHOT" # modern packet adapter
[libraries]
spigotApi = "org.spigotmc:spigot-api:1.12.2-R0.1-SNAPSHOT" # do not update
onePointEightPointEightNms = "org.github.spigot:1.8.8:1.8.8"
protocollib = "com.comphenix.protocol:ProtocolLib:5.3.0-SNAPSHOT"
packetEvents = "com.github.retrooper.packetevents:spigot:2.3.0"
buildIndra = { module = "net.kyori:indra-common", version = "3.1.3" }
buildNmcp = "com.gradleup.nmcp:com.gradleup.nmcp.gradle.plugin:0.0.8"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableException;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.PacketAdapterProvider;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -11,7 +14,8 @@
public final class PacketAdapterLoader {
private static final String MODERN = "modern",
V1_8_R3 = "v1_8_R3",
PACKET_EVENTS = "packetevents";
PACKET_EVENTS = "packetevents",
PROTOCOL_LIB = "protocollib";

private PacketAdapterLoader() {
}
Expand Down Expand Up @@ -41,6 +45,11 @@ private PacketAdapterLoader() {
return nmsClass;
}

Class<?> plibClass = tryLoadProtocolLib();
if (plibClass != null) {
return plibClass;
}

return tryLoadPacketEvents();
}

Expand Down Expand Up @@ -79,15 +88,32 @@ private PacketAdapterLoader() {
}
}

private static @Nullable Class<?> tryLoadPacketEvents() {
Class<?> nmsClass = tryLoadImplementationClass(PACKET_EVENTS);
if (nmsClass == null) {
private static @Nullable Class<?> tryLoadProtocolLib() {
Plugin loaderPlugin = JavaPlugin.getProvidingPlugin(PacketAdapterLoader.class);

Plugin plibPlugin = loaderPlugin.getServer().getPluginManager().getPlugin("ProtocolLib");
if (plibPlugin == null) {
return null;
}

PluginDescriptionFile d = loaderPlugin.getDescription();
if (!d.getDepend().contains(plibPlugin.getName()) && !d.getSoftDepend().contains(plibPlugin.getName())) {
return null;
}

try {
// ensure we are on a supported ProtocolLib version
Class.forName("com.comphenix.protocol.wrappers.WrappedTeamParameters");
return tryLoadImplementationClass(PROTOCOL_LIB);
} catch (ClassNotFoundException e) {
return null;
}
}

private static @Nullable Class<?> tryLoadPacketEvents() {
try {
Class.forName("com.github.retrooper.packetevents.PacketEvents");
return nmsClass;
return tryLoadImplementationClass(PACKET_EVENTS);
} catch (ClassNotFoundException ignored) {
return null;
}
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ project(":packet-adapter-base").projectDir = file("versions/packet-adapter-base"
include(":packetevents")
project(":packetevents").projectDir = file("versions/packetevents")

include(":protocollib")
project(":protocollib").projectDir = file("versions/protocollib")

include(":modern")
project(":modern").projectDir = file("versions/modern")

Expand Down
12 changes: 12 additions & 0 deletions versions/protocollib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
id("net.megavex.scoreboardlibrary.base-conventions")
}

repositories {
maven("https://repo.dmulloy2.net/repository/public/")
}

dependencies {
compileOnly(project(":scoreboard-library-packet-adapter-base"))
compileOnly(libs.protocollib)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.megavex.scoreboardlibrary.implementation.packetAdapter.protocollib;

import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedComponentStyle;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.jetbrains.annotations.NotNull;

// NOTE: ProtocolLib already has these utilities (AdventureComponentConverter)
// but they cannot be used as adventure is potentially relocated
public final class ComponentConversions {
private static final GsonComponentSerializer SERIALIZER;

static {
if (MinecraftVersion.NETHER_UPDATE.atOrAbove()) {
SERIALIZER = GsonComponentSerializer.gson();
} else {
SERIALIZER = GsonComponentSerializer.colorDownsamplingGson();
}
}

public static @NotNull WrappedChatComponent wrapAdventureComponent(@NotNull Component component) {
return WrappedChatComponent.fromJson(SERIALIZER.serialize(component));
}

public static @NotNull WrappedComponentStyle wrapAdventureStyle(@NotNull Style style) {
return WrappedComponentStyle.fromJson(SERIALIZER.serializer().toJsonTree(style));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package net.megavex.scoreboardlibrary.implementation.packetAdapter.protocollib;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedNumberFormat;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.translation.GlobalTranslator;
import net.megavex.scoreboardlibrary.api.objective.ObjectiveDisplaySlot;
import net.megavex.scoreboardlibrary.api.objective.ObjectiveRenderType;
import net.megavex.scoreboardlibrary.api.objective.ScoreFormat;
import net.megavex.scoreboardlibrary.implementation.commons.LegacyFormatUtil;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.PacketSender;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.PropertiesPacketType;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.objective.ObjectiveConstants;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.objective.ObjectivePacketAdapter;
import net.megavex.scoreboardlibrary.implementation.packetAdapter.util.LocalePacketUtil;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Optional;

import static net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection;

public class ObjectivePacketAdapterImpl implements ObjectivePacketAdapter {
private static final EnumWrappers.DisplaySlot[] DISPLAY_SLOTS = EnumWrappers.DisplaySlot.values();

private final ProtocolManager pm;
private final String objectiveName;
private PacketContainer removePacket;

public ObjectivePacketAdapterImpl(@NotNull ProtocolManager pm, @NotNull String objectiveName) {
this.pm = pm;
this.objectiveName = objectiveName;
}

@Override
public void display(@NotNull Collection<Player> players, @NotNull ObjectiveDisplaySlot slot) {
PacketContainer packet = pm.createPacket(PacketType.Play.Server.SCOREBOARD_DISPLAY_OBJECTIVE);
packet.getStrings().write(0, objectiveName);

int displaySlotIdx = ObjectiveConstants.displaySlotIndex(slot);
if (new MinecraftVersion(1, 20, 2).atOrAbove()) {
packet.getDisplaySlots().write(0, DISPLAY_SLOTS[displaySlotIdx]);
} else {
packet.getIntegers().write(0, displaySlotIdx);
}

for (Player player : players) {
pm.sendServerPacket(player, packet);
}
}

@Override
public void sendProperties(
@NotNull Collection<Player> players,
@NotNull PropertiesPacketType packetType,
@NotNull Component value,
@NotNull ObjectiveRenderType renderType,
@Nullable ScoreFormat scoreFormat
) {
PacketSender<PacketContainer> sender = pm::sendServerPacket;
LocalePacketUtil.sendLocalePackets(sender, players, locale -> {
EnumWrappers.RenderType wrappedRenderType;
switch (renderType) {
case INTEGER:
wrappedRenderType = EnumWrappers.RenderType.INTEGER;
break;
case HEARTS:
wrappedRenderType = EnumWrappers.RenderType.HEARTS;
break;
default:
throw new IllegalStateException();
}

PacketContainer packet = pm.createPacket(PacketType.Play.Server.SCOREBOARD_OBJECTIVE);
packet.getIntegers().write(0, ObjectiveConstants.mode(packetType));
packet.getStrings().write(0, objectiveName);
if (WrappedNumberFormat.isSupported() && scoreFormat != null) {
WrappedNumberFormat wrapped = ScoreFormatConverter.convert(locale, scoreFormat);
if (MinecraftVersion.v1_20_5.atOrAbove()) {
packet.getOptionals(BukkitConverters.getWrappedNumberFormatConverter())
.write(0, Optional.of(wrapped));
} else {
packet.getNumberFormats().write(0, wrapped);
}
}

Component translatedValue = GlobalTranslator.render(value, locale);
if (MinecraftVersion.AQUATIC_UPDATE.atOrAbove()) {
packet.getChatComponents().write(0, ComponentConversions.wrapAdventureComponent(translatedValue));
} else {
String legacyValue = LegacyFormatUtil.limitLegacyText(
legacySection().serialize(translatedValue),
ObjectiveConstants.LEGACY_VALUE_CHAR_LIMIT
);
packet.getStrings().write(1, legacyValue);
}

packet.getRenderTypes().write(0, wrappedRenderType);
return packet;
});
}

@Override
public void remove(@NotNull Collection<Player> players) {
if (removePacket == null) {
removePacket = pm.createPacket(PacketType.Play.Server.SCOREBOARD_OBJECTIVE);
removePacket.getIntegers().write(0, ObjectiveConstants.MODE_REMOVE);
removePacket.getStrings().write(0, objectiveName);
}

for (Player player : players) {
pm.sendServerPacket(player, removePacket);
}
}

@Override
public void sendScore(
@NotNull Collection<Player> players,
@NotNull String entry,
int value,
@Nullable Component display,
@Nullable ScoreFormat scoreFormat
) {

PacketSender<PacketContainer> sender = pm::sendServerPacket;
LocalePacketUtil.sendLocalePackets(sender, players, locale -> {
PacketContainer packet = pm.createPacket(PacketType.Play.Server.SCOREBOARD_SCORE);
packet.getScoreboardActions().write(0, EnumWrappers.ScoreboardAction.CHANGE);
packet.getStrings().write(0, entry)
.write(1, objectiveName);
packet.getIntegers().write(0, value);

if (WrappedNumberFormat.isSupported() && scoreFormat != null) {
WrappedNumberFormat wrapped = ScoreFormatConverter.convert(locale, scoreFormat);
if (MinecraftVersion.v1_20_5.atOrAbove()) {
packet.getOptionals(BukkitConverters.getWrappedNumberFormatConverter())
.write(1, Optional.of(wrapped));
} else {
packet.getNumberFormats().write(0, wrapped);
}
}

if (MinecraftVersion.v1_20_4.atOrAbove() && display != null) {
WrappedChatComponent wrapped = ComponentConversions.wrapAdventureComponent(GlobalTranslator.render(display, locale));
if (MinecraftVersion.v1_20_5.atOrAbove()) {
packet.getChatComponents().write(0, wrapped);
} else {
packet.getOptionals(BukkitConverters.getWrappedChatComponentConverter())
.write(0, Optional.of(wrapped));
}
}

return packet;
});

}

@Override
public void removeScore(@NotNull Collection<Player> players, @NotNull String entry) {
PacketContainer packet;
if (new MinecraftVersion(1, 20, 3).atOrAbove()) {
packet = pm.createPacket(PacketType.Play.Server.RESET_SCORE);
packet.getStrings().write(0, entry)
.write(1, objectiveName);
} else {
packet = pm.createPacket(PacketType.Play.Server.SCOREBOARD_SCORE);
packet.getScoreboardActions().write(0, EnumWrappers.ScoreboardAction.REMOVE);
packet.getStrings().write(0, entry)
.write(1, objectiveName);
}

for (Player player : players) {
pm.sendServerPacket(player, packet);
}
}
}
Loading