Skip to content

Commit

Permalink
refactor(spigot): use protocollib accessors for bossbar packets
Browse files Browse the repository at this point in the history
Fixes bossbars not working on MC 1.20.6 and above.
  • Loading branch information
diogotcorreia committed Jun 19, 2024
1 parent 11e6d8b commit fdcbbc4
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.rexcantor64.triton.Triton;
import com.rexcantor64.triton.language.item.SignLocation;
import com.rexcantor64.triton.packetinterceptor.protocollib.AdvancementsPacketHandler;
import com.rexcantor64.triton.packetinterceptor.protocollib.BossBarPacketHandler;
import com.rexcantor64.triton.packetinterceptor.protocollib.EntitiesPacketHandler;
import com.rexcantor64.triton.packetinterceptor.protocollib.HandlerFunction;
import com.rexcantor64.triton.packetinterceptor.protocollib.SignPacketHandler;
Expand All @@ -36,11 +37,9 @@
import com.rexcantor64.triton.wrappers.AdventureComponentWrapper;
import com.rexcantor64.triton.wrappers.WrappedClientConfiguration;
import com.rexcantor64.triton.wrappers.WrappedPlayerChatMessage;
import lombok.SneakyThrows;
import lombok.val;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import org.bukkit.Bukkit;
import org.bukkit.World;
Expand Down Expand Up @@ -84,9 +83,10 @@ public class ProtocolLibListener implements PacketListener, PacketInterceptor {
private final HandlerFunction ASYNC_PASSTHROUGH = asAsync((_packet, _player) -> {
});

private final SignPacketHandler signPacketHandler = new SignPacketHandler();
private final AdvancementsPacketHandler advancementsPacketHandler = AdvancementsPacketHandler.newInstance();
private final BossBarPacketHandler bossBarPacketHandler = new BossBarPacketHandler();
private final EntitiesPacketHandler entitiesPacketHandler = new EntitiesPacketHandler();
private final SignPacketHandler signPacketHandler = new SignPacketHandler();

private final SpigotMLP main;
private final List<HandlerFunction.HandlerType> allowedTypes;
Expand Down Expand Up @@ -202,22 +202,18 @@ private void setupPacketHandlers() {
}
packetHandlers.put(PacketType.Play.Server.WINDOW_ITEMS, asAsync(this::handleWindowItems));
packetHandlers.put(PacketType.Play.Server.SET_SLOT, asAsync(this::handleSetSlot));
if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { // 1.9+
// Bossbars were only added on MC 1.9
packetHandlers.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss));
}
if (MinecraftVersion.VILLAGE_UPDATE.atOrAbove()) { // 1.14+
// Villager merchant interface redesign on 1.14
packetHandlers.put(PacketType.Play.Server.OPEN_WINDOW_MERCHANT, asAsync(this::handleMerchantItems));
}


// External Packet Handlers
signPacketHandler.registerPacketTypes(packetHandlers);
if (advancementsPacketHandler != null) {
advancementsPacketHandler.registerPacketTypes(packetHandlers);
}
bossBarPacketHandler.registerPacketTypes(packetHandlers);
entitiesPacketHandler.registerPacketTypes(packetHandlers);
signPacketHandler.registerPacketTypes(packetHandlers);
}

/* PACKET HANDLERS */
Expand Down Expand Up @@ -583,58 +579,6 @@ private void handleSetSlot(PacketEvent packet, SpigotLanguagePlayer languagePlay
packet.getPacket().getItemModifier().writeSafely(0, item);
}

@SneakyThrows
private void handleBoss(PacketEvent packet, SpigotLanguagePlayer languagePlayer) {
if (!main.getConf().isBossbars()) return;

val uuid = packet.getPacket().getUUIDs().readSafely(0);
WrappedChatComponent bossbar;
Object actionObj = null;

if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
actionObj = packet.getPacket().getModifier().readSafely(1);
val method = actionObj.getClass().getMethod("a");
method.setAccessible(true);
val actionEnum = ((Enum<?>) method.invoke(actionObj)).ordinal();
if (actionEnum == 1) {
languagePlayer.removeBossbar(uuid);
return;
}
if (actionEnum != 0 && actionEnum != 3) return;

bossbar = WrappedChatComponent.fromHandle(NMSUtils.getDeclaredField(actionObj, "a"));
} else {
Action action = packet.getPacket().getEnumModifier(Action.class, 1).readSafely(0);
if (action == Action.REMOVE) {
languagePlayer.removeBossbar(uuid);
return;
}
if (action != Action.ADD && action != Action.UPDATE_NAME) return;

bossbar = packet.getPacket().getChatComponents().readSafely(0);
}


try {
languagePlayer.setBossbar(uuid, bossbar.getJson());
BaseComponent[] result = main.getLanguageParser().parseComponent(languagePlayer,
main.getConf().getBossbarSyntax(), ComponentSerializer.parse(bossbar.getJson()));
if (result == null)
result = new BaseComponent[]{new TranslatableComponent("")};
bossbar.setJson(ComponentSerializer.toString(result));
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
NMSUtils.setDeclaredField(actionObj, "a", bossbar.getHandle());
} else {
packet.getPacket().getChatComponents().writeSafely(0, bossbar);
}
} catch (RuntimeException e) {
// Catch 1.16 Hover 'contents' not being parsed correctly
// Has been fixed in newer versions of Spigot 1.16
Triton.get().getLogger()
.logError(e, "Could not parse a bossbar, so it was ignored. Bossbar: %1", bossbar.getJson());
}
}

@SuppressWarnings({"unchecked"})
private void handleMerchantItems(PacketEvent packet, SpigotLanguagePlayer languagePlayer) {
if (!main.getConf().isItems()) return;
Expand Down Expand Up @@ -905,30 +849,8 @@ public void refreshTabHeaderFooter(SpigotLanguagePlayer player, String header, S
}

@Override
@SneakyThrows
public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String json) {
if (!MinecraftVersion.COMBAT_UPDATE.atOrAbove()) {
// bossbar only works on 1.9+
return;
}

val bukkitPlayerOpt = player.toBukkit();
if (!bukkitPlayerOpt.isPresent()) return;
val bukkitPlayer = bukkitPlayerOpt.get();

PacketContainer packet = ProtocolLibrary.getProtocolManager().createPacket(PacketType.Play.Server.BOSS);
packet.getUUIDs().writeSafely(0, uuid);
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
val msg = WrappedChatComponent.fromJson(json);
val constructor = BOSSBAR_UPDATE_TITLE_ACTION_CLASS.getDeclaredConstructor(msg.getHandleType());
constructor.setAccessible(true);
val action = constructor.newInstance(msg.getHandle());
packet.getModifier().writeSafely(1, action);
} else {
packet.getEnumModifier(Action.class, 1).writeSafely(0, Action.UPDATE_NAME);
packet.getChatComponents().writeSafely(0, WrappedChatComponent.fromJson(json));
}
ProtocolLibrary.getProtocolManager().sendServerPacket(bukkitPlayer, packet, true);
public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String text) {
bossBarPacketHandler.refreshBossbar(player, uuid, text);
}

@Override
Expand Down Expand Up @@ -1060,11 +982,4 @@ private boolean isPlayerInventoryOpen(Player player) {
}
}

/**
* BossBar packet Action wrapper
*/
public enum Action {
ADD, REMOVE, UPDATE_PCT, UPDATE_NAME, UPDATE_STYLE, UPDATE_PROPERTIES
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package com.rexcantor64.triton.packetinterceptor.protocollib;

import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract;
import com.comphenix.protocol.utility.MinecraftReflection;
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.rexcantor64.triton.player.SpigotLanguagePlayer;
import lombok.val;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import net.md_5.bungee.chat.ComponentSerializer;

import java.util.Arrays;
import java.util.Map;
import java.util.UUID;

import static com.rexcantor64.triton.packetinterceptor.protocollib.HandlerFunction.asAsync;

public class BossBarPacketHandler extends PacketHandler {

private final Class<?> ACTION_ENUM_CLASS;
private final Class<?> OPERATION_INTERFACE;
private final MethodAccessor GET_OPERATION_ACTION_METHOD;
private final FieldAccessor ADD_COMPONENT_FIELD;
private final FieldAccessor UPDATE_COMPONENT_FIELD;
private final ConstructorAccessor UPDATE_CONSTRUCTOR;
private final EquivalentConverter<Action> ACTION_CONVERTER;
private final EquivalentConverter<WrappedChatComponent> COMPONENT_CONVERTER = BukkitConverters.getWrappedChatComponentConverter();

public BossBarPacketHandler() {
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
val innerClasses = PacketType.Play.Server.BOSS.getPacketClass().getDeclaredClasses();
ACTION_ENUM_CLASS = Arrays.stream(innerClasses)
.filter(Enum.class::isAssignableFrom)
.findAny()
.orElseThrow(() -> new RuntimeException("Cannot find boss bar action enum class"));

// the operation interface has a method that returns the operation type
OPERATION_INTERFACE = Arrays.stream(innerClasses)
.filter(Class::isInterface)
.filter(c -> !FuzzyReflection.fromClass(c).getMethodListByParameters(ACTION_ENUM_CLASS).isEmpty())
.findAny()
.orElseThrow(() -> new RuntimeException("Cannot find boss bar operation interface class"));
GET_OPERATION_ACTION_METHOD = Accessors.getMethodAccessor(
FuzzyReflection.fromClass(OPERATION_INTERFACE)
.getMethodByReturnTypeAndParameters("getType", ACTION_ENUM_CLASS)
);

// the add operation class has a chat component field, but does not have a chat component constructor
val addOperationClass = Arrays.stream(innerClasses)
.filter(OPERATION_INTERFACE::isAssignableFrom)
.filter(c -> !FuzzyReflection.fromClass(c, true).getFieldListByType(MinecraftReflection.getIChatBaseComponentClass()).isEmpty())
.filter(c -> FuzzyReflection.fromClass(c, true)
.getConstructorList(
FuzzyMethodContract.newBuilder()
.parameterExactType(MinecraftReflection.getIChatBaseComponentClass())
.build()
)
.isEmpty()
)
.findAny()
.orElseThrow(() -> new RuntimeException("Cannot find boss bar add operation class"));
ADD_COMPONENT_FIELD = Accessors.getFieldAccessor(addOperationClass, MinecraftReflection.getIChatBaseComponentClass(), true);

// the update operation class has a chat component field, and also has a chat component constructor
val updateOperationClass = Arrays.stream(innerClasses)
.filter(OPERATION_INTERFACE::isAssignableFrom)
.filter(c -> !FuzzyReflection.fromClass(c, true).getFieldListByType(MinecraftReflection.getIChatBaseComponentClass()).isEmpty())
.filter(c -> !FuzzyReflection.fromClass(c, true)
.getConstructorList(
FuzzyMethodContract.newBuilder()
.parameterExactType(MinecraftReflection.getIChatBaseComponentClass())
.build()
)
.isEmpty()
)
.findAny()
.orElseThrow(() -> new RuntimeException("Cannot find boss bar add operation class"));
UPDATE_COMPONENT_FIELD = Accessors.getFieldAccessor(updateOperationClass, MinecraftReflection.getIChatBaseComponentClass(), true);

UPDATE_CONSTRUCTOR = Accessors.getConstructorAccessor(
FuzzyReflection.fromClass(updateOperationClass, true)
.getConstructor(
FuzzyMethodContract.newBuilder()
.parameterExactType(MinecraftReflection.getIChatBaseComponentClass())
.build()
)
);

ACTION_CONVERTER = new EnumWrappers.EnumConverter<>(ACTION_ENUM_CLASS, Action.class);
} else {
ACTION_ENUM_CLASS = null;
OPERATION_INTERFACE = null;
GET_OPERATION_ACTION_METHOD = null;
ADD_COMPONENT_FIELD = null;
UPDATE_COMPONENT_FIELD = null;
UPDATE_CONSTRUCTOR = null;
ACTION_CONVERTER = null;
}
}

/**
* @return Whether the plugin should attempt to translate boss bars
*/
private boolean areBossBarsDisabled() {
return !getMain().getConf().isBossbars();
}

private void handleBoss_1_9(PacketEvent packet, SpigotLanguagePlayer languagePlayer) {
if (areBossBarsDisabled()) return;

val uuid = packet.getPacket().getUUIDs().readSafely(0);
Action action = packet.getPacket().getEnumModifier(Action.class, 1).readSafely(0);
if (action == Action.REMOVE) {
languagePlayer.removeBossbar(uuid);
return;
}
if (action != Action.ADD && action != Action.UPDATE_NAME) return;

WrappedChatComponent component = packet.getPacket().getChatComponents().readSafely(0);
try {
translateAndSaveBossBar(languagePlayer, uuid, component);
packet.getPacket().getChatComponents().writeSafely(0, component);
} catch (RuntimeException e) {
// Catch 1.16 Hover 'contents' not being parsed correctly
// Has been fixed in newer versions of Spigot 1.16
logger().logError(e, "Could not parse a bossbar, so it was ignored. Bossbar: %1", component.getJson());
}
}

private void handleBoss_1_17(PacketEvent packet, SpigotLanguagePlayer languagePlayer) {
if (areBossBarsDisabled()) return;

val uuid = packet.getPacket().getUUIDs().readSafely(0);
val operation = packet.getPacket().getSpecificModifier(OPERATION_INTERFACE).readSafely(0);
Action action = ACTION_CONVERTER.getSpecific(GET_OPERATION_ACTION_METHOD.invoke(operation));

if (action == Action.REMOVE) {
languagePlayer.removeBossbar(uuid);
return;
}

WrappedChatComponent component;
if (action == Action.ADD) {
component = COMPONENT_CONVERTER.getSpecific(ADD_COMPONENT_FIELD.get(operation));
} else if (action == Action.UPDATE_NAME) {
component = COMPONENT_CONVERTER.getSpecific(UPDATE_COMPONENT_FIELD.get(operation));
} else {
return;
}

translateAndSaveBossBar(languagePlayer, uuid, component);

if (action == Action.ADD) {
ADD_COMPONENT_FIELD.set(operation, COMPONENT_CONVERTER.getGeneric(component));
} else {
UPDATE_COMPONENT_FIELD.set(operation, COMPONENT_CONVERTER.getGeneric(component));
}
}

private void translateBossBar(SpigotLanguagePlayer languagePlayer, WrappedChatComponent component) {
BaseComponent[] result = getLanguageParser().parseComponent(
languagePlayer,
getConfig().getBossbarSyntax(),
ComponentSerializer.parse(component.getJson())
);
if (result == null) {
result = new BaseComponent[]{new TranslatableComponent("")};
}
component.setJson(ComponentSerializer.toString(result));
}

private void translateAndSaveBossBar(SpigotLanguagePlayer languagePlayer, UUID uuid, WrappedChatComponent component) {
languagePlayer.setBossbar(uuid, component.getJson());
translateBossBar(languagePlayer, component);
}

public void refreshBossbar(SpigotLanguagePlayer player, UUID uuid, String json) {
if (!MinecraftVersion.COMBAT_UPDATE.atOrAbove()) {
// bossbar only works on 1.9+
return;
}

val bukkitPlayerOpt = player.toBukkit();
if (!bukkitPlayerOpt.isPresent()) return;
val bukkitPlayer = bukkitPlayerOpt.get();

PacketContainer packet = createPacket(PacketType.Play.Server.BOSS);
packet.getUUIDs().writeSafely(0, uuid);
val msg = WrappedChatComponent.fromJson(json);
translateBossBar(player, msg);
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
Object operation = UPDATE_CONSTRUCTOR.invoke(COMPONENT_CONVERTER.getGeneric(msg));
// noinspection unchecked
packet.getSpecificModifier((Class<Object>) OPERATION_INTERFACE).writeSafely(0, operation);
} else {
packet.getEnumModifier(Action.class, 1).writeSafely(0, Action.UPDATE_NAME);
packet.getChatComponents().writeSafely(0, msg);
}
sendPacket(bukkitPlayer, packet, false);
}

@Override
public void registerPacketTypes(Map<PacketType, HandlerFunction> registry) {
if (MinecraftVersion.CAVES_CLIFFS_1.atOrAbove()) { // 1.17+
// Rework of packet to use subclasses instead of having all data on packet itself
registry.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss_1_17));
} else if (MinecraftVersion.COMBAT_UPDATE.atOrAbove()) { // 1.9+
// Bossbars were only added on MC 1.9
registry.put(PacketType.Play.Server.BOSS, asAsync(this::handleBoss_1_9));
}
}

/**
* BossBar packet Action wrapper
*/
public enum Action {
ADD, REMOVE, UPDATE_PCT, UPDATE_NAME, UPDATE_STYLE, UPDATE_PROPERTIES
}
}

0 comments on commit fdcbbc4

Please sign in to comment.