diff --git a/README.md b/README.md
index 37ff91d..27ee78f 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ See [FAQ](#FAQ)
dev.cerus.maps
common
- 3.8.8
+ 3.8.9
provided
@@ -59,7 +59,7 @@ See [FAQ](#FAQ)
dev.cerus.maps
plugin
- 3.8.8
+ 3.8.9
provided
diff --git a/bukkit-16_R3/pom.xml b/bukkit-16_R3/pom.xml
index d4869dd..498b09d 100644
--- a/bukkit-16_R3/pom.xml
+++ b/bukkit-16_R3/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-17_R1/pom.xml b/bukkit-17_R1/pom.xml
index b228c63..a2e76f3 100644
--- a/bukkit-17_R1/pom.xml
+++ b/bukkit-17_R1/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-18_R1/pom.xml b/bukkit-18_R1/pom.xml
index f1c8d8e..5b6cf9a 100644
--- a/bukkit-18_R1/pom.xml
+++ b/bukkit-18_R1/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-18_R2/pom.xml b/bukkit-18_R2/pom.xml
index bb9ab8f..47379e6 100644
--- a/bukkit-18_R2/pom.xml
+++ b/bukkit-18_R2/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-19_R1/pom.xml b/bukkit-19_R1/pom.xml
index e9bc3c9..b6bea07 100644
--- a/bukkit-19_R1/pom.xml
+++ b/bukkit-19_R1/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-19_R2/pom.xml b/bukkit-19_R2/pom.xml
index ba0cb89..0f23b78 100644
--- a/bukkit-19_R2/pom.xml
+++ b/bukkit-19_R2/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/bukkit-19_R3/pom.xml b/bukkit-19_R3/pom.xml
index 1ab6079..1ee180d 100644
--- a/bukkit-19_R3/pom.xml
+++ b/bukkit-19_R3/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-19_R3
diff --git a/bukkit-20_R1/pom.xml b/bukkit-20_R1/pom.xml
index a2e8118..fe82a64 100644
--- a/bukkit-20_R1/pom.xml
+++ b/bukkit-20_R1/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-20_R1
diff --git a/bukkit-20_R2/pom.xml b/bukkit-20_R2/pom.xml
index c2d0041..333bd6c 100644
--- a/bukkit-20_R2/pom.xml
+++ b/bukkit-20_R2/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-20_R2
diff --git a/bukkit-20_R3/pom.xml b/bukkit-20_R3/pom.xml
index 6fce17a..748a55a 100644
--- a/bukkit-20_R3/pom.xml
+++ b/bukkit-20_R3/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-20_R3
diff --git a/bukkit-20_R4/pom.xml b/bukkit-20_R4/pom.xml
index 8a7f659..3b851f0 100644
--- a/bukkit-20_R4/pom.xml
+++ b/bukkit-20_R4/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-20_R4
diff --git a/bukkit-21_R1/pom.xml b/bukkit-21_R1/pom.xml
index 53fa376..0e76deb 100644
--- a/bukkit-21_R1/pom.xml
+++ b/bukkit-21_R1/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-21_R1
diff --git a/bukkit-21_R2/pom.xml b/bukkit-21_R2/pom.xml
index 97071c0..8b55599 100644
--- a/bukkit-21_R2/pom.xml
+++ b/bukkit-21_R2/pom.xml
@@ -6,7 +6,7 @@
dev.cerus.maps
parent
- 3.8.8
+ 3.8.9
bukkit-21_R2
diff --git a/bukkit-21_R3/pom.xml b/bukkit-21_R3/pom.xml
new file mode 100644
index 0000000..3a4c8a2
--- /dev/null
+++ b/bukkit-21_R3/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+
+ dev.cerus.maps
+ parent
+ 3.8.9
+
+
+ bukkit-21_R3
+
+
+ 16
+ 16
+ UTF-8
+
+
+
+
+ dev.cerus.maps
+ common
+ ${parent.version}
+ provided
+
+
+ org.bukkit
+ craftbukkit
+ 1.21.4-R0.1-SNAPSHOT
+ provided
+
+
+
+
diff --git a/bukkit-21_R3/src/main/java/dev/cerus/maps/version/PacketHandler21R3.java b/bukkit-21_R3/src/main/java/dev/cerus/maps/version/PacketHandler21R3.java
new file mode 100644
index 0000000..9617bee
--- /dev/null
+++ b/bukkit-21_R3/src/main/java/dev/cerus/maps/version/PacketHandler21R3.java
@@ -0,0 +1,110 @@
+package dev.cerus.maps.version;
+
+import dev.cerus.maps.api.version.PacketListener;
+import io.netty.channel.ChannelDuplexHandler;
+import io.netty.channel.ChannelHandlerContext;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import net.minecraft.network.protocol.game.ClientboundBlockChangedAckPacket;
+import net.minecraft.network.protocol.game.PacketPlayInBlockDig;
+import net.minecraft.network.protocol.game.PacketPlayInBlockPlace;
+import net.minecraft.network.protocol.game.PacketPlayInUseEntity;
+import net.minecraft.network.protocol.game.PacketPlayInUseItem;
+import net.minecraft.world.EnumHand;
+import net.minecraft.world.phys.MovingObjectPositionBlock;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.craftbukkit.v1_21_R3.block.CraftBlock;
+import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class PacketHandler21R3 extends ChannelDuplexHandler {
+
+ private static final Field actionField;
+ private static final Object attackAction;
+
+ static {
+ try {
+ actionField = PacketPlayInUseEntity.class.getDeclaredField("c");
+ actionField.setAccessible(true);
+ final Field attackActionField = PacketPlayInUseEntity.class.getDeclaredField("e");
+ attackActionField.setAccessible(true);
+ attackAction = attackActionField.get(null);
+ } catch (final NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final Player player;
+ private final PacketListener listener;
+ private final JavaPlugin plugin;
+
+ public PacketHandler21R3(final Player player, final PacketListener listener, final JavaPlugin plugin) {
+ this.player = player;
+ this.listener = listener;
+ this.plugin = plugin;
+ }
+
+ private static Object getAction(final PacketPlayInUseEntity packet) {
+ try {
+ return actionField.get(packet);
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static EnumHand getHand(final PacketPlayInUseEntity packet) {
+ try {
+ final Object action = getAction(packet);
+ final Field handField = Arrays.stream(action.getClass().getDeclaredFields())
+ .filter(field -> field.getType() == EnumHand.class)
+ .findAny().orElse(null);
+ if (handField == null) {
+ return null;
+ }
+ handField.setAccessible(true);
+ return (EnumHand) handField.get(action);
+ } catch (final IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
+ if ((((msg instanceof final PacketPlayInUseItem useItem && useItem.b() != EnumHand.b) || msg instanceof PacketPlayInBlockPlace) && this.listener.handlePlayerRightClick(this.player))
+ || (msg instanceof final PacketPlayInUseEntity useEntity && getHand(useEntity) != EnumHand.b && (getAction(useEntity) == attackAction ? this.listener.handlePlayerLeftClick(this.player) : this.listener.handlePlayerRightClick(this.player)))
+ || (msg instanceof PacketPlayInBlockDig && this.listener.handlePlayerLeftClick(this.player))) {
+ if (msg instanceof final PacketPlayInBlockDig dig) {
+ // To prevent de-syncs we need to tell the client that the block has not changed
+ final Location location = new Location(
+ this.player.getWorld(),
+ dig.b().u(),
+ dig.b().v(),
+ dig.b().w()
+ );
+ // If we don't acknowledge the client's block change it won't accept further block change packets
+ ((CraftPlayer) this.player).getHandle().f.a(new ClientboundBlockChangedAckPacket(dig.g()));
+ Bukkit.getScheduler().runTask(this.plugin, () -> this.player.sendBlockChange(location, location.getBlock().getBlockData()));
+ }
+ if (msg instanceof final PacketPlayInUseItem useItem) {
+ // To prevent de-syncs we need to tell the client that the block has not changed
+ final MovingObjectPositionBlock pos = useItem.e();
+ if (pos != null && pos.a() != null) {
+ final Location location = new Location(
+ this.player.getWorld(),
+ pos.b().u(),
+ pos.b().v(),
+ pos.b().w()
+ ).getBlock().getRelative(CraftBlock.notchToBlockFace(pos.c())).getLocation();
+ // If we don't acknowledge the client's block change it won't accept further block change packets
+ ((CraftPlayer) this.player).getHandle().f.a(new ClientboundBlockChangedAckPacket(useItem.f()));
+ Bukkit.getScheduler().runTask(this.plugin, () -> this.player.sendBlockChange(location, location.getBlock().getBlockData()));
+ }
+ }
+ return;
+ }
+ super.channelRead(ctx, msg);
+ }
+
+}
diff --git a/bukkit-21_R3/src/main/java/dev/cerus/maps/version/VersionAdapter21R3.java b/bukkit-21_R3/src/main/java/dev/cerus/maps/version/VersionAdapter21R3.java
new file mode 100644
index 0000000..a944311
--- /dev/null
+++ b/bukkit-21_R3/src/main/java/dev/cerus/maps/version/VersionAdapter21R3.java
@@ -0,0 +1,185 @@
+package dev.cerus.maps.version;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.mojang.serialization.JsonOps;
+import dev.cerus.maps.api.ClientsideMap;
+import dev.cerus.maps.api.Frame;
+import dev.cerus.maps.api.version.PacketListener;
+import dev.cerus.maps.api.version.VersionAdapter;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.NetworkManager;
+import net.minecraft.network.chat.ComponentSerialization;
+import net.minecraft.network.chat.IChatBaseComponent;
+import net.minecraft.network.protocol.Packet;
+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.MapIcon;
+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.Location;
+import org.bukkit.Material;
+import org.bukkit.Particle;
+import org.bukkit.block.BlockFace;
+import org.bukkit.craftbukkit.v1_21_R3.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_21_R3.inventory.CraftItemStack;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.meta.MapMeta;
+import org.bukkit.plugin.java.JavaPlugin;
+
+public class VersionAdapter21R3 implements VersionAdapter {
+
+ private Field netManField;
+
+ @Override
+ public void spawnBarrierParticle(final Player player, final Location loc) {
+ player.spawnParticle(Particle.BLOCK_MARKER, loc, 1, Material.BARRIER.createBlockData());
+ }
+
+ @Override
+ public Object makeMapPacket(final boolean ignoreBounds, final ClientsideMap map) {
+ final int x = ignoreBounds ? 0 : map.getX();
+ final int y = ignoreBounds ? 0 : map.getY();
+ final int w = ignoreBounds ? 128 : Math.max(1, map.getWidth());
+ final int h = ignoreBounds ? 128 : Math.max(1, map.getHeight());
+
+ final byte[] data;
+ if (ignoreBounds) {
+ data = map.getData();
+ } else {
+ data = new byte[w * h];
+ for (int xx = 0; xx < w; ++xx) {
+ for (int yy = 0; yy < h; ++yy) {
+ data[xx + yy * w] = map.getData()[x + xx + (y + yy) * 128];
+ }
+ }
+ }
+
+ return new PacketPlayOutMap(
+ new MapId(map.getId()),
+ (byte) 0,
+ true,
+ map.getMarkers().stream()
+ .map(cursor -> new MapIcon(
+ BuiltInRegistries.ar.c(cursor.getType()).get(),
+ cursor.getCompressedX(),
+ cursor.getCompressedY(),
+ cursor.getDirection(),
+ !cursor.hasCaption() ? Optional.empty() : parse(cursor.getCaptionString())
+ ))
+ .collect(Collectors.toList()),
+ new WorldMap.c(
+ x,
+ y,
+ w,
+ h,
+ data
+ )
+ );
+ }
+
+ private Optional parse(String s) {
+ if (s == null) {
+ return Optional.empty();
+ }
+ JsonElement element = JsonParser.parseString(s);
+ if (element == null) {
+ return Optional.empty();
+ }
+ return ComponentSerialization.a.parse(JsonOps.INSTANCE, element).result();
+ }
+
+ @Override
+ public Object makeFramePacket(final int frameId, final boolean visible, final ClientsideMap map) {
+ final org.bukkit.inventory.ItemStack mapItem = new org.bukkit.inventory.ItemStack(Material.FILLED_MAP, 1);
+ final MapMeta mapMeta = (MapMeta) mapItem.getItemMeta();
+ mapMeta.setMapId(map.getId());
+ mapItem.setItemMeta(mapMeta);
+
+ final List> dwItems = Arrays.asList(
+ new DataWatcher.c<>(8, DataWatcherRegistry.h, CraftItemStack.asNMSCopy(mapItem)),
+ new DataWatcher.c<>(0, DataWatcherRegistry.a, (byte) (visible ? 0 : 0x20))
+ );
+ return new PacketPlayOutEntityMetadata(frameId, dwItems);
+ }
+
+ @Override
+ public Object makeFrameSpawnPacket(final Frame frame) {
+ return new PacketPlayOutSpawnEntity(
+ frame.getEntityId(),
+ UUID.randomUUID(),
+ frame.getPosX(),
+ frame.getPosY(),
+ frame.getPosZ(),
+ frame.getFacing() == BlockFace.DOWN ? 90 : frame.getFacing() == BlockFace.UP ? -90 : 0,
+ switch (frame.getFacing()) {
+ case NORTH -> -180;
+ case EAST -> -90;
+ case WEST -> 90;
+ default -> 0;
+ },
+ frame.isGlowing() ? EntityTypes.af : EntityTypes.as,
+ switch (frame.getFacing()) {
+ case UP -> 1;
+ case NORTH -> 2;
+ case SOUTH -> 3;
+ case WEST -> 4;
+ case EAST -> 5;
+ default -> 0;
+ },
+ new Vec3D(0, 0, 0),
+ switch (frame.getFacing()) {
+ case NORTH -> -180;
+ case EAST -> -90;
+ case WEST -> 90;
+ default -> 0;
+ }
+ );
+ }
+
+ @Override
+ public Object makeFrameDespawnPacket(final Frame frame) {
+ return new PacketPlayOutEntityDestroy(frame.getEntityId());
+ }
+
+ @Override
+ public void sendPacket(final Player player, final Object packet) {
+ ((CraftPlayer) player).getHandle().f.b((Packet>) packet);
+ }
+
+ @Override
+ public void inject(final Player player, final PacketListener listener, final JavaPlugin plugin) {
+ final NetworkManager networkManager;
+ try {
+ networkManager = this.getNetworkManager(((CraftPlayer) player).getHandle().f);
+ } catch (final IllegalAccessException | NoSuchFieldException e) {
+ plugin.getLogger().log(Level.WARNING, "Failed to inject packet handler into player %s".formatted(player.getName()), e);
+ return;
+ }
+ networkManager.n.pipeline().addBefore("packet_handler", "maps_listener", new PacketHandler21R3(player, listener, plugin));
+ }
+
+ private NetworkManager getNetworkManager(final PlayerConnection b) throws IllegalAccessException, NoSuchFieldException {
+ if (this.netManField == null) {
+ this.netManField = ServerCommonPacketListenerImpl.class.getDeclaredField("e");
+ this.netManField.setAccessible(true);
+ }
+ return (NetworkManager) this.netManField.get(b);
+ }
+
+}
diff --git a/common/pom.xml b/common/pom.xml
index 0f075c5..96122d7 100644
--- a/common/pom.xml
+++ b/common/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
diff --git a/plugin/pom.xml b/plugin/pom.xml
index 1ea53dd..0d3a9a3 100644
--- a/plugin/pom.xml
+++ b/plugin/pom.xml
@@ -5,7 +5,7 @@
parent
dev.cerus.maps
- 3.8.8
+ 3.8.9
4.0.0
@@ -134,6 +134,12 @@
${parent.version}
compile
+
+ dev.cerus.maps
+ bukkit-21_R3
+ ${parent.version}
+ compile
+
diff --git a/plugin/src/main/java/dev/cerus/maps/version/VersionAdapterFactory.java b/plugin/src/main/java/dev/cerus/maps/version/VersionAdapterFactory.java
index 9412b07..5131b00 100644
--- a/plugin/src/main/java/dev/cerus/maps/version/VersionAdapterFactory.java
+++ b/plugin/src/main/java/dev/cerus/maps/version/VersionAdapterFactory.java
@@ -6,7 +6,7 @@
public class VersionAdapterFactory {
public static final String MIN_VER = "1.16.5";
- public static final String MAX_VER = "1.21";
+ public static final String MAX_VER = "1.21.4";
public VersionAdapter makeAdapter() {
String version = Bukkit.getVersion();
@@ -26,6 +26,7 @@ public VersionAdapter makeAdapter() {
case "1.20.5", "1.20.6" -> new VersionAdapter20R4();
case "1.21", "1.21.1" -> new VersionAdapter21R1();
case "1.21.2", "1.21.3" -> new VersionAdapter21R2();
+ case "1.21.4" -> new VersionAdapter21R3();
default -> null;
};
}
diff --git a/pom.xml b/pom.xml
index 203a2f1..aea729e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
dev.cerus.maps
parent
pom
- 3.8.8
+ 3.8.9
common
bukkit-16_R3
@@ -23,6 +23,7 @@
bukkit-20_R4
bukkit-21_R1
bukkit-21_R2
+ bukkit-21_R3
plugin