diff --git a/README.md b/README.md
index 03ebd96..910e20b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Visualizes the crafting process
## Compatibility
-Compatible with Spigot & Paper 1.16.5, 1.17, 1.17.1, 1.18.1, 1.18.2, 1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1 and 1.20.2
+Compatible with Spigot & Paper 1.16.5, 1.17, 1.17.1, 1.18.1, 1.18.2, 1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.4 and 1.20.5
## How it works
diff --git a/api/src/main/java/dev/cerus/visualcrafting/api/version/FakeMap.java b/api/src/main/java/dev/cerus/visualcrafting/api/version/FakeMap.java
index e34faa7..3eb8d4a 100644
--- a/api/src/main/java/dev/cerus/visualcrafting/api/version/FakeMap.java
+++ b/api/src/main/java/dev/cerus/visualcrafting/api/version/FakeMap.java
@@ -11,10 +11,12 @@ public class FakeMap {
private final int id;
private final byte[] data;
+ private final Object handle;
- FakeMap(final int id) {
+ FakeMap(final int id, Object handle) {
this.id = id;
this.data = new byte[128 * 128];
+ this.handle = handle;
}
public void setPixel(final int x, final int y, final byte color) {
@@ -41,4 +43,7 @@ byte[] getData() {
return this.data;
}
+ Object getHandle() {
+ return handle;
+ }
}
diff --git a/api/src/main/java/dev/cerus/visualcrafting/api/version/VersionAdapter.java b/api/src/main/java/dev/cerus/visualcrafting/api/version/VersionAdapter.java
index 6cef2da..5326193 100644
--- a/api/src/main/java/dev/cerus/visualcrafting/api/version/VersionAdapter.java
+++ b/api/src/main/java/dev/cerus/visualcrafting/api/version/VersionAdapter.java
@@ -80,6 +80,18 @@ public void updateItemDisplay(final int displayId, final FakeItemDisplay itemDis
*/
public abstract void destroyEntity(int entityId);
+ /**
+ * Helper method for creating a new fake map
+ *
+ * @param id The map's id
+ * @param handle The map's nms handle
+ *
+ * @return A new fake map
+ */
+ protected FakeMap createMap(final int id, Object handle) {
+ return new FakeMap(id, handle);
+ }
+
/**
* Helper method for creating a new fake map
*
@@ -88,7 +100,7 @@ public void updateItemDisplay(final int displayId, final FakeItemDisplay itemDis
* @return A new fake map
*/
protected FakeMap createMap(final int id) {
- return new FakeMap(id);
+ return createMap(id, null);
}
/**
@@ -102,6 +114,17 @@ protected byte[] getMapData(final FakeMap fakeMap) {
return fakeMap.getData();
}
+ /**
+ * Get a fake map's nms handle
+ *
+ * @param fakeMap The fake map
+ *
+ * @return The nms handle
+ */
+ protected Object getMapHandle(FakeMap fakeMap) {
+ return fakeMap.getHandle();
+ }
+
/**
* Create a new fake map
*
diff --git a/bukkit-20R4/pom.xml b/bukkit-20R4/pom.xml
new file mode 100644
index 0000000..50cc3af
--- /dev/null
+++ b/bukkit-20R4/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ dev.cerus.visualcrafting
+ parent
+ 1.2.0
+
+
+ bukkit-20R4
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ dev.cerus.visualcrafting
+ api
+ ${project.parent.version}
+ provided
+
+
+
+ org.bukkit
+ craftbukkit
+ 1.20.5-R0.1-SNAPSHOT
+ provided
+
+
+
+
\ No newline at end of file
diff --git a/bukkit-20R4/src/main/java/dev/cerus/visualcrafting/v20r4/VersionAdapter20R4.java b/bukkit-20R4/src/main/java/dev/cerus/visualcrafting/v20r4/VersionAdapter20R4.java
new file mode 100644
index 0000000..3b9efbd
--- /dev/null
+++ b/bukkit-20R4/src/main/java/dev/cerus/visualcrafting/v20r4/VersionAdapter20R4.java
@@ -0,0 +1,232 @@
+package dev.cerus.visualcrafting.v20r4;
+
+import com.mojang.math.Transformation;
+import dev.cerus.visualcrafting.api.config.Config;
+import dev.cerus.visualcrafting.api.version.FakeItemDisplay;
+import dev.cerus.visualcrafting.api.version.FakeMap;
+import dev.cerus.visualcrafting.api.version.Feature;
+import dev.cerus.visualcrafting.api.version.VersionAdapter;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.UUID;
+import java.util.function.BiConsumer;
+import net.minecraft.network.NetworkManager;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.game.PacketPlayInUseEntity;
+import net.minecraft.network.protocol.game.PacketPlayOutEntityDestroy;
+import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
+import net.minecraft.network.protocol.game.PacketPlayOutMap;
+import net.minecraft.network.protocol.game.PacketPlayOutSpawnEntity;
+import net.minecraft.network.syncher.DataWatcher;
+import net.minecraft.network.syncher.DataWatcherRegistry;
+import net.minecraft.server.network.PlayerConnection;
+import net.minecraft.server.network.ServerCommonPacketListenerImpl;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.world.level.saveddata.maps.MapId;
+import net.minecraft.world.level.saveddata.maps.WorldMap;
+import net.minecraft.world.phys.Vec3D;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Rotation;
+import org.bukkit.block.BlockFace;
+import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftItemStack;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class VersionAdapter20R4 extends VersionAdapter {
+
+ private Config config;
+ private BiConsumer entityClickCallback;
+ private int nextEntityId;
+ private int nextMapId;
+ private Field netManField;
+
+ private NetworkManager getNetworkManager(final PlayerConnection b) {
+ try {
+ if (this.netManField == null) {
+ this.netManField = ServerCommonPacketListenerImpl.class.getDeclaredField("e");
+ this.netManField.setAccessible(true);
+ }
+ return (NetworkManager) this.netManField.get(b);
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void init(final Config config, final BiConsumer entityClickCallback) {
+ this.config = config;
+ this.entityClickCallback = entityClickCallback;
+ this.nextEntityId = config.entityIdRangeMin();
+ this.nextMapId = config.mapIdRangeMin();
+ }
+
+ @Override
+ public void inject(final Player player) {
+ if (this.config.enablePacketListening()) {
+ final NetworkManager netMan = this.getNetworkManager(((CraftPlayer) player).getHandle().c);
+ netMan.n.pipeline()
+ .addBefore("packet_handler", "visual_crafting", new ChannelDuplexHandler() {
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ if (msg instanceof final PacketPlayInUseEntity useEntity) {
+ VersionAdapter20R4.this.handlePacketIn(player, useEntity);
+ }
+ super.channelRead(ctx, msg);
+ }
+ });
+ }
+ }
+
+ private void handlePacketIn(final Player player, final PacketPlayInUseEntity packet) {
+ try {
+ final Field a = packet.getClass().getDeclaredField("b");
+ a.setAccessible(true);
+ this.entityClickCallback.accept(player, (Integer) a.get(packet));
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public int spawnItemFrame(final Location location, final BlockFace direction) {
+ final int eid = this.getNewEntityId();
+ final PacketPlayOutSpawnEntity packet = new PacketPlayOutSpawnEntity(
+ eid,
+ UUID.randomUUID(),
+ location.getBlockX(),
+ location.getBlockY(),
+ location.getBlockZ(),
+ direction == BlockFace.DOWN ? 90 : direction == BlockFace.UP ? -90 : 0,
+ switch (direction) {
+ case NORTH -> -180;
+ case EAST -> -90;
+ case WEST -> 90;
+ default -> 0;
+ },
+ EntityTypes.ai,
+ switch (direction) {
+ case UP -> 1;
+ case NORTH -> 2;
+ case SOUTH -> 3;
+ case WEST -> 4;
+ case EAST -> 5;
+ default -> 0;
+ },
+ new Vec3D(0, 0, 0),
+ switch (direction) {
+ case NORTH -> -180;
+ case EAST -> -90;
+ case WEST -> 90;
+ default -> 0;
+ }
+ );
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ return eid;
+ }
+
+ @Override
+ public int spawnItemDisplay(final FakeItemDisplay itemDisplay) {
+ final int eid = this.getNewEntityId();
+ final PacketPlayOutSpawnEntity packet = new PacketPlayOutSpawnEntity(
+ eid,
+ UUID.randomUUID(),
+ itemDisplay.getLocation().getBlockX(),
+ itemDisplay.getLocation().getBlockY(),
+ itemDisplay.getLocation().getBlockZ(),
+ 0,
+ 0,
+ EntityTypes.ah,
+ 0,
+ new Vec3D(0, 0, 0),
+ 0
+ );
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ return eid;
+ }
+
+ @Override
+ public void updateItemFrame(final int frameId, final ItemStack itemStack, final Rotation rotation, final boolean invisible) {
+ final PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(frameId, Arrays.asList(
+ new DataWatcher.c<>(8, DataWatcherRegistry.h, CraftItemStack.asNMSCopy(itemStack)),
+ new DataWatcher.c<>(9, DataWatcherRegistry.b, rotation.ordinal()),
+ new DataWatcher.c<>(0, DataWatcherRegistry.a, (byte) (invisible ? 0x20 : 0))
+ ));
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ }
+
+ @Override
+ public void updateItemDisplay(final int displayId, final FakeItemDisplay itemDisplay) {
+ final Transformation nmsTransf = new Transformation(itemDisplay.getTransformationMatrix());
+ final PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(displayId, Arrays.asList(
+ new DataWatcher.c<>(11, DataWatcherRegistry.D, nmsTransf.d()),
+ new DataWatcher.c<>(12, DataWatcherRegistry.D, nmsTransf.f()),
+ new DataWatcher.c<>(13, DataWatcherRegistry.E, nmsTransf.e()),
+ new DataWatcher.c<>(14, DataWatcherRegistry.E, nmsTransf.g()),
+ new DataWatcher.c<>(23, DataWatcherRegistry.h, CraftItemStack.asNMSCopy(itemDisplay.getItemStack())),
+ new DataWatcher.c<>(24, DataWatcherRegistry.a, (byte) itemDisplay.getTransform().ordinal())
+ ));
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ }
+
+ @Override
+ public void destroyEntity(final int entityId) {
+ final PacketPlayOutEntityDestroy packet = new PacketPlayOutEntityDestroy(entityId);
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ }
+
+ @Override
+ public FakeMap createMap() {
+ int mapId = getNewMapId();
+ return this.createMap(mapId, new MapId(mapId));
+ }
+
+ @Override
+ public void sendMap(final FakeMap map) {
+ final PacketPlayOutMap packet = new PacketPlayOutMap(
+ (MapId) getMapHandle(map),
+ (byte) 0,
+ true,
+ Collections.emptyList(),
+ new WorldMap.b(0,
+ 0,
+ 128,
+ 128,
+ this.getMapData(map))
+ );
+ Bukkit.getOnlinePlayers().forEach(player -> this.sendPacket(player, packet));
+ }
+
+ @Override
+ public EnumSet getImplementedFeatures() {
+ return FEATURES_DISPLAY;
+ }
+
+ private void sendPacket(final Player player, final Packet> packet) {
+ ((CraftPlayer) player).getHandle().c.b(packet);
+ }
+
+ private int getNewEntityId() {
+ if (this.nextEntityId >= this.config.entityIdRangeMax()) {
+ this.nextEntityId = this.config.entityIdRangeMin();
+ return this.nextEntityId;
+ } else {
+ return this.nextEntityId++;
+ }
+ }
+
+ private int getNewMapId() {
+ if (this.nextMapId >= this.config.mapIdRangeMax()) {
+ this.nextMapId = this.config.mapIdRangeMin();
+ return this.nextMapId;
+ } else {
+ return this.nextMapId++;
+ }
+ }
+
+}
diff --git a/plugin/pom.xml b/plugin/pom.xml
index 6fc5a28..9e272a5 100644
--- a/plugin/pom.xml
+++ b/plugin/pom.xml
@@ -89,6 +89,12 @@
${project.parent.version}
compile
+
+ dev.cerus.visualcrafting
+ bukkit-20R4
+ ${project.parent.version}
+ compile
+
dev.cerus.maps
@@ -130,7 +136,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.8.1
+ 3.13.0
${maven.compiler.target}
@@ -139,7 +145,7 @@
org.apache.maven.plugins
maven-shade-plugin
- 3.4.0
+ 3.5.1
diff --git a/plugin/src/main/java/dev/cerus/visualcrafting/plugin/VisualCraftingPlugin.java b/plugin/src/main/java/dev/cerus/visualcrafting/plugin/VisualCraftingPlugin.java
index 58f0dd1..43a62c6 100644
--- a/plugin/src/main/java/dev/cerus/visualcrafting/plugin/VisualCraftingPlugin.java
+++ b/plugin/src/main/java/dev/cerus/visualcrafting/plugin/VisualCraftingPlugin.java
@@ -19,6 +19,7 @@
import dev.cerus.visualcrafting.v20r1.VersionAdapter20R1;
import dev.cerus.visualcrafting.v20r2.VersionAdapter20R2;
import dev.cerus.visualcrafting.v20r3.VersionAdapter20R3;
+import dev.cerus.visualcrafting.v20r4.VersionAdapter20R4;
import java.io.File;
import java.net.URL;
import java.security.CodeSource;
@@ -56,6 +57,7 @@ public void onEnable() {
case "1.20", "1.20.1" -> new VersionAdapter20R1();
case "1.20.2" -> new VersionAdapter20R2();
case "1.20.3", "1.20.4" -> new VersionAdapter20R3();
+ case "1.20.5" -> new VersionAdapter20R4();
default -> null;
};
if (versionAdapter == null) {
diff --git a/pom.xml b/pom.xml
index 4011a06..e48d985 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
bukkit-20R1
bukkit-20R2
bukkit-20R3
+ bukkit-20R4