diff --git a/core/pom.xml b/core/pom.xml
index f29be2e8..bb63d95f 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -134,5 +134,9 @@
${project.groupId}
illusion-nms-v26_2
+
+ ${project.groupId}
+ illusion-nms-v26_3
+
diff --git a/core/src/main/java/me/filoghost/holographicdisplays/core/HolographicDisplaysCore.java b/core/src/main/java/me/filoghost/holographicdisplays/core/HolographicDisplaysCore.java
index 9fb92f39..257b808a 100644
--- a/core/src/main/java/me/filoghost/holographicdisplays/core/HolographicDisplaysCore.java
+++ b/core/src/main/java/me/filoghost/holographicdisplays/core/HolographicDisplaysCore.java
@@ -42,7 +42,7 @@ public void enable(Plugin plugin, ErrorCollector errorCollector) throws PluginEn
nmsManager = NMSVersion.getCurrent().createNMSManager(errorCollector);
} catch (UnknownVersionException e) {
throw new PluginEnableException(
- "Holographic Displays only supports Spigot from 1.8 to 26.2, but your server is running "
+ "Holographic Displays only supports Spigot from 1.8 to 26.3, but your server is running "
+ Bukkit.getServer().getVersion());
} catch (OutdatedVersionException e) {
throw new PluginEnableException(
diff --git a/core/src/main/java/me/filoghost/holographicdisplays/core/NMSVersion.java b/core/src/main/java/me/filoghost/holographicdisplays/core/NMSVersion.java
index d91bf7af..021a9455 100644
--- a/core/src/main/java/me/filoghost/holographicdisplays/core/NMSVersion.java
+++ b/core/src/main/java/me/filoghost/holographicdisplays/core/NMSVersion.java
@@ -7,7 +7,7 @@
import me.filoghost.fcommons.logging.ErrorCollector;
import me.filoghost.holographicdisplays.nms.common.NMSManager;
-import me.filoghost.holographicdisplays.nms.v26_2.VersionNMSManager;
+import me.filoghost.holographicdisplays.nms.v26_3.VersionNMSManager;
import org.bukkit.Bukkit;
/**
@@ -44,7 +44,8 @@ public enum NMSVersion {
/* 1.21.9- 1.21.10 */ v1_21_R6(me.filoghost.holographicdisplays.nms.v1_21_R6.VersionNMSManager::new),
/* 1.21.11 */ v1_21_R7(me.filoghost.holographicdisplays.nms.v1_21_R7.VersionNMSManager::new),
/* 26.1.2 */ v26_1(me.filoghost.holographicdisplays.nms.v26_1.VersionNMSManager::new),
- /* 26.2 - X */ v26_2(VersionNMSManager::new),
+ /* 26.2 */ v26_2(me.filoghost.holographicdisplays.nms.v26_2.VersionNMSManager::new),
+ /* 26.3 - X */ v26_3(VersionNMSManager::new),
/* Other versions */ UNKNOWN(NMSManagerFactory.unknownVersion());
private static final NMSVersion CURRENT_VERSION = detectCurrentVersion();
@@ -85,6 +86,8 @@ private static NMSVersion detectVersion(String bukkitVersion, String nmsPackageN
return v26_1;
case "26.2":
return v26_2;
+ case "26.3":
+ return v26_3;
default:
return UNKNOWN;
}
diff --git a/core/src/test/java/me/filoghost/holographicdisplays/core/NMSVersionStringParserTest.java b/core/src/test/java/me/filoghost/holographicdisplays/core/NMSVersionStringParserTest.java
index dd93f9b5..b2359fb8 100644
--- a/core/src/test/java/me/filoghost/holographicdisplays/core/NMSVersionStringParserTest.java
+++ b/core/src/test/java/me/filoghost/holographicdisplays/core/NMSVersionStringParserTest.java
@@ -26,9 +26,11 @@ static Stream normalizeYearBasedVersionTestArguments() {
// Spigot format, unchanged since before the year-based versioning switch
Arguments.of("26.1.2-R0.1-SNAPSHOT", "26.1.2"),
Arguments.of("26.2-R0.1-SNAPSHOT", "26.2"),
+ Arguments.of("26.3-R0.1-SNAPSHOT", "26.3"),
// Paper release format: ".build.-"
Arguments.of("26.2.build.62-beta", "26.2"),
+ Arguments.of("26.3.build.1-alpha", "26.3"),
Arguments.of("26.1.2.build.5-default", "26.1.2"),
// Paper development build format: "-DEV-@"
diff --git a/nms/pom.xml b/nms/pom.xml
index f4d6c5d2..bffa2ef9 100644
--- a/nms/pom.xml
+++ b/nms/pom.xml
@@ -50,6 +50,7 @@
v1_21_r7
v26_1
v26_2
+ v26_3
diff --git a/nms/v26_3/pom.xml b/nms/v26_3/pom.xml
new file mode 100644
index 00000000..ca0f59d8
--- /dev/null
+++ b/nms/v26_3/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ studio.magemonkey
+ illusion-nms
+ 3.0.15-SNAPSHOT
+
+
+ illusion-nms-v26_3
+ Illusion NMS v26_3
+
+
+
+ ${project.groupId}
+ illusion-nms-common
+
+
+
+ org.spigotmc
+ spigot
+ 26.3-R0.1-SNAPSHOT
+
+ provided
+
+
+ org.spigotmc
+ spigot-api
+ 26.3-R0.1-SNAPSHOT
+ provided
+
+
+
+ org.joml
+ joml
+ 1.10.9
+ provided
+
+
+
+
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherKey.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherKey.java
new file mode 100644
index 00000000..2e24872b
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherKey.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import io.netty.handler.codec.EncoderException;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.syncher.EntityDataSerializer;
+import net.minecraft.network.syncher.EntityDataSerializers;
+import net.minecraft.world.item.ItemStack;
+
+import java.util.Optional;
+
+class DataWatcherKey {
+
+ private static final EntityDataSerializer BYTE_SERIALIZER = EntityDataSerializers.BYTE;
+ private static final EntityDataSerializer INT_SERIALIZER = EntityDataSerializers.INT;
+ private static final EntityDataSerializer BOOLEAN_SERIALIZER = EntityDataSerializers.BOOLEAN;
+ private static final EntityDataSerializer ITEM_STACK_SERIALIZER = EntityDataSerializers.ITEM_STACK;
+ private static final EntityDataSerializer> OPTIONAL_CHAT_COMPONENT_SERIALIZER = EntityDataSerializers.OPTIONAL_COMPONENT;
+
+ static final DataWatcherKey ENTITY_STATUS = new DataWatcherKey<>(0, BYTE_SERIALIZER);
+ static final DataWatcherKey> CUSTOM_NAME = new DataWatcherKey<>(2, OPTIONAL_CHAT_COMPONENT_SERIALIZER);
+ static final DataWatcherKey CUSTOM_NAME_VISIBILITY = new DataWatcherKey<>(3, BOOLEAN_SERIALIZER);
+ static final DataWatcherKey ITEM_STACK = new DataWatcherKey<>(8, ITEM_STACK_SERIALIZER);
+ static final DataWatcherKey ARMOR_STAND_STATUS = new DataWatcherKey<>(15, BYTE_SERIALIZER);
+ static final DataWatcherKey SLIME_SIZE = new DataWatcherKey<>(18, INT_SERIALIZER);
+
+ private final int index;
+ private final EntityDataSerializer serializer;
+ private final int serializerTypeID;
+
+ private DataWatcherKey(int index, EntityDataSerializer serializer) {
+ this.index = index;
+ this.serializer = serializer;
+ this.serializerTypeID = EntityDataSerializers.getSerializedId(serializer);
+ if (serializerTypeID < 0) {
+ throw new EncoderException("Could not find serializer ID of " + serializer);
+ }
+ }
+
+ int getIndex() {
+ return index;
+ }
+
+ EntityDataSerializer getSerializer() {
+ return serializer;
+ }
+
+ int getSerializerTypeID() {
+ return serializerTypeID;
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherPacketBuilder.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherPacketBuilder.java
new file mode 100644
index 00000000..102e53b3
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/DataWatcherPacketBuilder.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.fcommons.Strings;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.syncher.SynchedEntityData;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.CraftChatMessage;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+abstract class DataWatcherPacketBuilder {
+
+ private static final int MAX_CUSTOM_NAME_LENGTH = 5000;
+
+ protected final EntityID entityId;
+ protected List> watchers = new ArrayList<>();
+
+ public DataWatcherPacketBuilder(EntityID entityId) {
+ this.entityId = entityId;
+ }
+
+ DataWatcherPacketBuilder setInvisible() {
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.ENTITY_STATUS.getIndex(),
+ DataWatcherKey.ENTITY_STATUS.getSerializer(),
+ (byte) 0x20));
+ return this;
+ }
+
+ DataWatcherPacketBuilder setArmorStandMarker() {
+ setInvisible();
+ // Small, no gravity, no base plate, marker
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.ARMOR_STAND_STATUS.getIndex(),
+ DataWatcherKey.ARMOR_STAND_STATUS.getSerializer(),
+ (byte) (0x01 | 0x02 | 0x08 | 0x10)));
+ return this;
+ }
+
+ DataWatcherPacketBuilder setCustomName(String customName) {
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.CUSTOM_NAME.getIndex(),
+ DataWatcherKey.CUSTOM_NAME.getSerializer(),
+ getCustomNameDataWatcherValue(customName)));
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.CUSTOM_NAME_VISIBILITY.getIndex(),
+ DataWatcherKey.CUSTOM_NAME_VISIBILITY.getSerializer(),
+ !Strings.isEmpty(customName)));
+ return this;
+ }
+
+ private Optional getCustomNameDataWatcherValue(String customName) {
+ customName = Strings.truncate(customName, MAX_CUSTOM_NAME_LENGTH);
+ if (!Strings.isEmpty(customName)) {
+ return Optional.of(CraftChatMessage.fromString(customName, false, true)[0]);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ DataWatcherPacketBuilder setItemStack(ItemStack itemStack) {
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.ITEM_STACK.getIndex(),
+ DataWatcherKey.ITEM_STACK.getSerializer(),
+ CraftItemStack.asNMSCopy(itemStack)));
+ return this;
+ }
+
+ DataWatcherPacketBuilder setSlimeSmall() {
+ watchers.add(new SynchedEntityData.DataValue<>(DataWatcherKey.SLIME_SIZE.getIndex(),
+ DataWatcherKey.SLIME_SIZE.getSerializer(),
+ 0));
+ return this;
+ }
+
+ T build() {
+ return createPacket();
+ }
+
+ abstract T createPacket();
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityDestroyNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityDestroyNMSPacket.java
new file mode 100644
index 00000000..84053aba
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityDestroyNMSPacket.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
+
+class EntityDestroyNMSPacket extends VersionNMSPacket {
+
+ private final Packet> rawPacket;
+
+ EntityDestroyNMSPacket(EntityID entityID) {
+ this.rawPacket = new ClientboundRemoveEntitiesPacket(entityID.getNumericID());
+ }
+
+ EntityDestroyNMSPacket(EntityID entityID1, EntityID entityID2) {
+ this.rawPacket = new ClientboundRemoveEntitiesPacket(entityID1.getNumericID(), entityID2.getNumericID());
+ }
+
+ @Override
+ Packet> getRawPacket() {
+ return rawPacket;
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMetadataNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMetadataNMSPacket.java
new file mode 100644
index 00000000..50da7a10
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMetadataNMSPacket.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.syncher.SynchedEntityData;
+
+import java.util.List;
+
+class EntityMetadataNMSPacket extends VersionNMSPacket {
+
+ private final Packet> rawPacket;
+
+ private EntityMetadataNMSPacket(EntityID entityId, List> watchers) {
+ this.rawPacket = new ClientboundSetEntityDataPacket(entityId.getNumericID(), watchers);
+ }
+
+ @Override
+ Packet> getRawPacket() {
+ return rawPacket;
+ }
+
+ public static DataWatcherPacketBuilder builder(EntityID entityID) {
+ return new Builder(entityID);
+ }
+
+
+ private static class Builder extends DataWatcherPacketBuilder {
+ public Builder(EntityID entityId) {
+ super(entityId);
+ }
+
+ @Override
+ EntityMetadataNMSPacket createPacket() {
+ return new EntityMetadataNMSPacket(entityId, watchers);
+ }
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMountNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMountNMSPacket.java
new file mode 100644
index 00000000..6ca98677
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityMountNMSPacket.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket;
+
+import java.lang.reflect.Constructor;
+
+class EntityMountNMSPacket extends VersionNMSPacket {
+
+ private final Packet> rawPacket;
+
+ EntityMountNMSPacket(EntityID vehicleEntityID, EntityID passengerEntityID) {
+ PacketByteBuffer packetByteBuffer = PacketByteBuffer.get();
+
+ // We'll leave this as a byte buffer for now, since I think the ClientboundSetPassengersPacket constructor
+ // requires modifying the vehicle entity to have the passenger before we can create this packet
+ packetByteBuffer.writeVarInt(vehicleEntityID.getNumericID());
+ packetByteBuffer.writeVarIntArray(passengerEntityID.getNumericID());
+
+ try {
+ Constructor ctor =
+ ClientboundSetPassengersPacket.class.getDeclaredConstructor(FriendlyByteBuf.class);
+ ctor.setAccessible(true);
+ this.rawPacket = ctor.newInstance(packetByteBuffer.getInternalSerializer());
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create EntityMountNMSPacket", e);
+ }
+ }
+
+ @Override
+ Packet> getRawPacket() {
+ return rawPacket;
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntitySpawnNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntitySpawnNMSPacket.java
new file mode 100644
index 00000000..bdcdd0d1
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntitySpawnNMSPacket.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.common.PositionCoordinates;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.phys.Vec3;
+
+class EntitySpawnNMSPacket extends VersionNMSPacket {
+
+ private final Packet> rawPacket;
+
+ EntitySpawnNMSPacket(EntityID entityID,
+ EntityType> entityType,
+ PositionCoordinates position,
+ double positionOffsetY) {
+ this.rawPacket = new ClientboundAddEntityPacket(
+ entityID.getNumericID(),
+ entityID.getUUID(),
+ position.getX(),
+ position.getY() + positionOffsetY,
+ position.getZ(),
+ // x-rot (yaw)
+ 0,
+ // y-rot (pitch)
+ 0,
+ // The entity type to spawn
+ entityType,
+ // Apply velocity? 1 if present and zero (otherwise by default a random velocity is applied)
+ entityType == EntityTypeConstants.ITEM ? 1 : 0,
+ // Velocity
+ new Vec3(0, 0, 0),
+ // y-head-rot (???)
+ 0
+ );
+ }
+
+ @Override
+ Packet> getRawPacket() {
+ return rawPacket;
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTeleportNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTeleportNMSPacket.java
new file mode 100644
index 00000000..361f8fdd
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTeleportNMSPacket.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.common.PositionCoordinates;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
+import net.minecraft.world.entity.PositionMoveRotation;
+import net.minecraft.world.entity.Relative;
+import net.minecraft.world.phys.Vec3;
+
+import java.util.HashSet;
+import java.util.Set;
+
+class EntityTeleportNMSPacket extends VersionNMSPacket {
+
+ private final Packet> rawPacket;
+
+ EntityTeleportNMSPacket(EntityID entityID, PositionCoordinates position, double positionOffsetY) {
+ Vec3 pos = new Vec3(position.getX(), position.getY() + positionOffsetY, position.getZ());
+ Vec3 delta = new Vec3(0, 0, 0);
+ PositionMoveRotation positionMoveRotation = new PositionMoveRotation(pos, delta, 0, 0);
+ Set relatives = new HashSet<>();
+
+ this.rawPacket = new ClientboundTeleportEntityPacket(entityID.getNumericID(), positionMoveRotation, relatives, false);
+ }
+
+ @Override
+ Packet> getRawPacket() {
+ return rawPacket;
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTypeConstants.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTypeConstants.java
new file mode 100644
index 00000000..d9a87716
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/EntityTypeConstants.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.entity.decoration.ArmorStand;
+import net.minecraft.world.entity.item.ItemEntity;
+import net.minecraft.world.entity.monster.cubemob.Slime;
+
+// Sourced from net.minecraft.world.entity.EntityTypes
+class EntityTypeConstants {
+
+ static final EntityType ARMOR_STAND = EntityTypes.ARMOR_STAND;
+ static final EntityType ITEM = EntityTypes.ITEM;
+ static final EntityType SLIME = EntityTypes.SLIME;
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/InboundPacketHandler.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/InboundPacketHandler.java
new file mode 100644
index 00000000..4ba076cf
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/InboundPacketHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import me.filoghost.fcommons.logging.Log;
+import me.filoghost.holographicdisplays.nms.common.NMSErrors;
+import me.filoghost.holographicdisplays.nms.common.PacketListener;
+import net.minecraft.network.protocol.game.ServerboundInteractPacket;
+import org.bukkit.entity.Player;
+
+class InboundPacketHandler extends ChannelInboundHandlerAdapter {
+
+ public static final String HANDLER_NAME = "holographic_displays_listener";
+
+ private final Player player;
+ private final PacketListener packetListener;
+
+ InboundPacketHandler(Player player, PacketListener packetListener) {
+ this.player = player;
+ this.packetListener = packetListener;
+ }
+
+ @Override
+ public void channelRead(ChannelHandlerContext context, Object packet) throws Exception {
+ try {
+ if (packet instanceof ServerboundInteractPacket) {
+ int entityID = ((ServerboundInteractPacket) packet).entityId();
+ boolean cancel = packetListener.onAsyncEntityInteract(player, entityID);
+ if (cancel) {
+ return;
+ }
+ }
+ } catch (Throwable t) {
+ Log.warning(NMSErrors.EXCEPTION_ON_PACKET_READ, t);
+ }
+ super.channelRead(context, packet);
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/PacketByteBuffer.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/PacketByteBuffer.java
new file mode 100644
index 00000000..021ae036
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/PacketByteBuffer.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import io.netty.buffer.Unpooled;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import org.bukkit.craftbukkit.CraftRegistry;
+
+import java.util.UUID;
+
+class PacketByteBuffer {
+
+ private static final ThreadLocal LOCAL_INSTANCE = ThreadLocal.withInitial(PacketByteBuffer::new);
+
+ private final RegistryFriendlyByteBuf serializer;
+
+ static PacketByteBuffer get() {
+ PacketByteBuffer instance = LOCAL_INSTANCE.get();
+ instance.clear();
+ return instance;
+ }
+
+ private PacketByteBuffer() {
+ this.serializer =
+ new RegistryFriendlyByteBuf(Unpooled.buffer(), CraftRegistry.getMinecraftRegistry());
+ }
+
+ void writeBoolean(boolean flag) {
+ serializer.writeBoolean(flag);
+ }
+
+ void writeByte(int i) {
+ serializer.writeByte(i);
+ }
+
+ void writeShort(int i) {
+ serializer.writeShort(i);
+ }
+
+ void writeInt(int i) {
+ serializer.writeInt(i);
+ }
+
+ void writeDouble(double d) {
+ serializer.writeDouble(d);
+ }
+
+ void writeVarInt(int i) {
+ serializer.writeVarInt(i);
+ }
+
+ void writeVarIntArray(int i1) {
+ writeVarInt(1);
+ writeVarInt(i1);
+ }
+
+ void writeVarIntArray(int i1, int i2) {
+ writeVarInt(2);
+ writeVarInt(i1);
+ writeVarInt(i2);
+ }
+
+ void writeUUID(UUID uuid) {
+ serializer.writeUUID(uuid);
+ }
+
+ void writeDataWatcherEntry(DataWatcherKey key, T value) {
+ serializer.writeByte(key.getIndex());
+ writeVarInt(key.getSerializerTypeID());
+ key.getSerializer().codec().encode(serializer, value);
+ }
+
+ void writeDataWatcherEntriesEnd() {
+ serializer.writeByte(0xFF);
+ }
+
+ public RegistryFriendlyByteBuf getInternalSerializer() {
+ return serializer;
+ }
+
+ void clear() {
+ serializer.clear();
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionClickableNMSPacketEntity.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionClickableNMSPacketEntity.java
new file mode 100644
index 00000000..b5f35b9b
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionClickableNMSPacketEntity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.common.PositionCoordinates;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import me.filoghost.holographicdisplays.nms.common.PacketGroup;
+import me.filoghost.holographicdisplays.nms.common.entity.ClickableNMSPacketEntity;
+
+class VersionClickableNMSPacketEntity implements ClickableNMSPacketEntity {
+
+ private final EntityID slimeID;
+
+ VersionClickableNMSPacketEntity(EntityID slimeID) {
+ this.slimeID = slimeID;
+ }
+
+ @Override
+ public EntityID getID() {
+ return slimeID;
+ }
+
+ @Override
+ public PacketGroup newSpawnPackets(PositionCoordinates position) {
+ return PacketGroup.of(
+ new EntitySpawnNMSPacket(slimeID, EntityTypeConstants.SLIME, position, SLIME_Y_OFFSET),
+ EntityMetadataNMSPacket.builder(slimeID)
+ .setInvisible()
+ .setSlimeSmall() // Required for a correct client-side collision box
+ .build()
+ );
+ }
+
+ @Override
+ public PacketGroup newTeleportPackets(PositionCoordinates position) {
+ return new EntityTeleportNMSPacket(slimeID, position, SLIME_Y_OFFSET);
+ }
+
+ @Override
+ public PacketGroup newDestroyPackets() {
+ return new EntityDestroyNMSPacket(slimeID);
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionItemNMSPacketEntity.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionItemNMSPacketEntity.java
new file mode 100644
index 00000000..73f60d27
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionItemNMSPacketEntity.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.common.PositionCoordinates;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import me.filoghost.holographicdisplays.nms.common.PacketGroup;
+import me.filoghost.holographicdisplays.nms.common.entity.ItemNMSPacketEntity;
+import org.bukkit.inventory.ItemStack;
+
+class VersionItemNMSPacketEntity implements ItemNMSPacketEntity {
+
+ private final EntityID itemID;
+ private final EntityID vehicleID;
+
+ VersionItemNMSPacketEntity(EntityID itemID, EntityID vehicleID) {
+ this.itemID = itemID;
+ this.vehicleID = vehicleID;
+ }
+
+ @Override
+ public PacketGroup newSpawnPackets(PositionCoordinates position, ItemStack itemStack) {
+ return PacketGroup.of(
+ new EntitySpawnNMSPacket(vehicleID, EntityTypeConstants.ARMOR_STAND, position, ITEM_Y_OFFSET),
+ EntityMetadataNMSPacket.builder(vehicleID)
+ .setArmorStandMarker()
+ .build(),
+ new EntitySpawnNMSPacket(itemID, EntityTypeConstants.ITEM, position, ITEM_Y_OFFSET),
+ EntityMetadataNMSPacket.builder(itemID)
+ .setItemStack(itemStack)
+ .build(),
+ new EntityMountNMSPacket(vehicleID, itemID)
+ );
+ }
+
+ @Override
+ public PacketGroup newChangePackets(ItemStack itemStack) {
+ return EntityMetadataNMSPacket.builder(itemID)
+ .setItemStack(itemStack)
+ .build();
+ }
+
+ @Override
+ public PacketGroup newTeleportPackets(PositionCoordinates position) {
+ return new EntityTeleportNMSPacket(vehicleID, position, ITEM_Y_OFFSET);
+ }
+
+ @Override
+ public PacketGroup newDestroyPackets() {
+ return new EntityDestroyNMSPacket(itemID, vehicleID);
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSManager.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSManager.java
new file mode 100644
index 00000000..0c87122a
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSManager.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoop;
+import me.filoghost.fcommons.logging.ErrorCollector;
+import me.filoghost.fcommons.logging.Log;
+import me.filoghost.fcommons.reflection.ReflectField;
+import me.filoghost.holographicdisplays.nms.common.*;
+import me.filoghost.holographicdisplays.nms.common.entity.ClickableNMSPacketEntity;
+import me.filoghost.holographicdisplays.nms.common.entity.ItemNMSPacketEntity;
+import me.filoghost.holographicdisplays.nms.common.entity.TextNMSPacketEntity;
+import net.minecraft.network.Connection;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.network.ServerCommonPacketListenerImpl;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class VersionNMSManager implements NMSManager {
+
+ private static final ReflectField NETWORK_MANAGER_FIELD =
+ ReflectField.lookup(Connection.class, ServerCommonPacketListenerImpl.class, "connection");
+ private final Supplier entityIDGenerator;
+
+ public VersionNMSManager(ErrorCollector errorCollector) {
+ this.entityIDGenerator = getEntityIDGenerator(errorCollector);
+
+ // Force initialization of class to eventually throw exceptions early
+ DataWatcherKey.ENTITY_STATUS.getIndex();
+ }
+
+ /*
+ * As of 26.2, the entity ID counter is no longer a static field on Entity, but is exposed through
+ * ServerLevel#getNextEntityId(), which is backed by a single counter shared by all levels.
+ */
+ private Supplier getEntityIDGenerator(ErrorCollector errorCollector) {
+ try {
+ List worlds = Bukkit.getWorlds();
+ if (worlds.isEmpty()) {
+ throw new IllegalStateException("No worlds are loaded");
+ }
+ ServerLevel serverLevel = ((CraftWorld) worlds.get(0)).getHandle();
+ return serverLevel::getNextEntityId;
+ } catch (Exception e) {
+ errorCollector.add(e, NMSErrors.EXCEPTION_GETTING_ENTITY_ID_GENERATOR);
+ return new FallbackEntityIDGenerator();
+ }
+ }
+
+ private EntityID newEntityID() {
+ return new EntityID(entityIDGenerator);
+ }
+
+ @Override
+ public TextNMSPacketEntity newTextPacketEntity() {
+ return new VersionTextNMSPacketEntity(newEntityID());
+ }
+
+ @Override
+ public ItemNMSPacketEntity newItemPacketEntity() {
+ return new VersionItemNMSPacketEntity(newEntityID(), newEntityID());
+ }
+
+ @Override
+ public ClickableNMSPacketEntity newClickablePacketEntity() {
+ return new VersionClickableNMSPacketEntity(newEntityID());
+ }
+
+ @Override
+ public void injectPacketListener(Player player, PacketListener packetListener) {
+ modifyPipeline(player, (ChannelPipeline pipeline) -> {
+ ChannelHandler currentListener = pipeline.get(InboundPacketHandler.HANDLER_NAME);
+ if (currentListener != null) {
+ pipeline.remove(InboundPacketHandler.HANDLER_NAME);
+ }
+ pipeline.addBefore("packet_handler",
+ InboundPacketHandler.HANDLER_NAME,
+ new InboundPacketHandler(player, packetListener));
+ });
+ }
+
+ @Override
+ public void uninjectPacketListener(Player player) {
+ modifyPipeline(player, (ChannelPipeline pipeline) -> {
+ ChannelHandler currentListener = pipeline.get(InboundPacketHandler.HANDLER_NAME);
+ if (currentListener != null) {
+ pipeline.remove(InboundPacketHandler.HANDLER_NAME);
+ }
+ });
+ }
+
+ /*
+ * Modifying the pipeline in the main thread can cause deadlocks, delays and other concurrency issues,
+ * which can be avoided by using the event loop. Thanks to ProtocolLib for this insight.
+ */
+ private void modifyPipeline(Player player, Consumer pipelineModifierTask) {
+ ServerGamePacketListenerImpl playerConnection = ((CraftPlayer) player).getHandle().connection;
+ Connection networkManager;
+ try {
+ networkManager = NETWORK_MANAGER_FIELD.get(playerConnection);
+ } catch (ReflectiveOperationException e) {
+ Log.warning(NMSErrors.EXCEPTION_MODIFYING_CHANNEL_PIPELINE, e);
+ return;
+ }
+ Channel channel = networkManager.channel;
+ if (channel == null) {
+ return;
+ }
+ EventLoop eventLoop = channel.eventLoop();
+
+ Runnable safeModifierTask = () -> {
+ if (!player.isOnline()) {
+ return;
+ }
+ try {
+ pipelineModifierTask.accept(channel.pipeline());
+ } catch (Exception e) {
+ Log.warning(NMSErrors.EXCEPTION_MODIFYING_CHANNEL_PIPELINE, e);
+ }
+ };
+
+ if (eventLoop.inEventLoop()) {
+ safeModifierTask.run();
+ } else {
+ eventLoop.execute(safeModifierTask);
+ }
+ }
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSPacket.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSPacket.java
new file mode 100644
index 00000000..b3e8aa76
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionNMSPacket.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.nms.common.PacketGroup;
+import net.minecraft.network.protocol.Packet;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+
+abstract class VersionNMSPacket implements PacketGroup {
+
+ @Override
+ public void sendTo(Player player) {
+ ((CraftPlayer) player).getHandle().connection.send(getRawPacket());
+ }
+
+ abstract Packet> getRawPacket();
+
+}
diff --git a/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionTextNMSPacketEntity.java b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionTextNMSPacketEntity.java
new file mode 100644
index 00000000..15944979
--- /dev/null
+++ b/nms/v26_3/src/main/java/me/filoghost/holographicdisplays/nms/v26_3/VersionTextNMSPacketEntity.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) filoghost and contributors
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package me.filoghost.holographicdisplays.nms.v26_3;
+
+import me.filoghost.holographicdisplays.common.PositionCoordinates;
+import me.filoghost.holographicdisplays.nms.common.EntityID;
+import me.filoghost.holographicdisplays.nms.common.IndividualTextPacketGroup;
+import me.filoghost.holographicdisplays.nms.common.PacketGroup;
+import me.filoghost.holographicdisplays.nms.common.entity.TextNMSPacketEntity;
+
+class VersionTextNMSPacketEntity implements TextNMSPacketEntity {
+
+ private final EntityID armorStandID;
+
+ VersionTextNMSPacketEntity(EntityID armorStandID) {
+ this.armorStandID = armorStandID;
+ }
+
+ @Override
+ public PacketGroup newSpawnPackets(PositionCoordinates position, String text) {
+ return PacketGroup.of(
+ new EntitySpawnNMSPacket(armorStandID, EntityTypeConstants.ARMOR_STAND, position, ARMOR_STAND_Y_OFFSET),
+ EntityMetadataNMSPacket.builder(armorStandID)
+ .setArmorStandMarker()
+ .setCustomName(text)
+ .build()
+ );
+ }
+
+ @Override
+ public IndividualTextPacketGroup newSpawnPackets(PositionCoordinates position) {
+ return IndividualTextPacketGroup.of(
+ new EntitySpawnNMSPacket(armorStandID, EntityTypeConstants.ARMOR_STAND, position, ARMOR_STAND_Y_OFFSET),
+ (String text) -> EntityMetadataNMSPacket.builder(armorStandID)
+ .setArmorStandMarker()
+ .setCustomName(text)
+ .build()
+ );
+ }
+
+ @Override
+ public PacketGroup newChangePackets(String text) {
+ return EntityMetadataNMSPacket.builder(armorStandID)
+ .setCustomName(text)
+ .build();
+ }
+
+ @Override
+ public IndividualTextPacketGroup newChangePackets() {
+ return IndividualTextPacketGroup.of(
+ (String text) -> EntityMetadataNMSPacket.builder(armorStandID)
+ .setCustomName(text)
+ .build()
+ );
+ }
+
+ @Override
+ public PacketGroup newTeleportPackets(PositionCoordinates position) {
+ return new EntityTeleportNMSPacket(armorStandID, position, ARMOR_STAND_Y_OFFSET);
+ }
+
+ @Override
+ public PacketGroup newDestroyPackets() {
+ return new EntityDestroyNMSPacket(armorStandID);
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index d6591683..af141e51 100644
--- a/pom.xml
+++ b/pom.xml
@@ -207,6 +207,11 @@
illusion-nms-v26_2
${nms.version}
+
+ ${project.groupId}
+ illusion-nms-v26_3
+ ${nms.version}
+
org.spigotmc
spigot-api