diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index e45f72b..387fa9e 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -29,6 +29,8 @@
+
+
diff --git a/NMS-v1_21_R1/pom.xml b/NMS-v1_21_R1/pom.xml
new file mode 100644
index 0000000..598c369
--- /dev/null
+++ b/NMS-v1_21_R1/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+ MapReflectionAPI
+ tech.sbdevelopment
+ ${revision}
+
+ 4.0.0
+
+ MapReflectionAPI-NMS-v1_21_R1
+
+
+ 1.21-R0.1-SNAPSHOT
+ 21
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${jdk.version}
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.1.1
+
+ true
+
+
+
+
+
+
+
+ org.bukkit
+ craftbukkit
+ ${NMSVersion}
+ provided
+
+
+ tech.sbdevelopment
+ MapReflectionAPI-API
+ ${revision}
+ provided
+
+
+
\ No newline at end of file
diff --git a/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapSender_v1_21_R1.java b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapSender_v1_21_R1.java
new file mode 100644
index 0000000..c349201
--- /dev/null
+++ b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapSender_v1_21_R1.java
@@ -0,0 +1,139 @@
+/*
+ * This file is part of MapReflectionAPI.
+ * Copyright (c) 2022-2023 inventivetalent / SBDevelopment - All Rights Reserved
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package tech.sbdevelopment.mapreflectionapi.nms;
+
+import net.minecraft.network.protocol.game.PacketPlayOutMap;
+import net.minecraft.world.level.saveddata.maps.MapId;
+import net.minecraft.world.level.saveddata.maps.WorldMap;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import tech.sbdevelopment.mapreflectionapi.MapReflectionAPI;
+import tech.sbdevelopment.mapreflectionapi.api.ArrayImage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The {@link MapSender_v1_21_R1} sends the Map packets to players.
+ */
+public class MapSender_v1_21_R1 {
+ private static final List sendQueue = new ArrayList<>();
+ private static int senderID = -1;
+
+ private MapSender_v1_21_R1() {
+ }
+
+ /**
+ * Add a map to the send queue
+ *
+ * @param id The ID of the map
+ * @param content The {@link ArrayImage} to view on the map
+ * @param player The {@link Player} to view for
+ */
+ public static void addToQueue(final int id, final ArrayImage content, final Player player) {
+ QueuedMap toSend = new QueuedMap(id, content, player);
+ if (sendQueue.contains(toSend)) return;
+ sendQueue.add(toSend);
+
+ runSender();
+ }
+
+ /**
+ * Cancels a senderID in the sender queue
+ *
+ * @param s The senderID to cancel
+ */
+ public static void cancelID(int s) {
+ sendQueue.removeIf(queuedMap -> queuedMap.id == s);
+ }
+
+ /**
+ * Run the sender task
+ */
+ private static void runSender() {
+ if (Bukkit.getScheduler().isQueued(senderID) || Bukkit.getScheduler().isCurrentlyRunning(senderID) || sendQueue.isEmpty())
+ return;
+
+ senderID = Bukkit.getScheduler().scheduleSyncRepeatingTask(MapReflectionAPI.getInstance(), () -> {
+ if (sendQueue.isEmpty()) return;
+
+ for (int i = 0; i < Math.min(sendQueue.size(), 10 + 1); i++) {
+ QueuedMap current = sendQueue.get(0);
+ if (current == null) return;
+
+ sendMap(current.id, current.image, current.player);
+
+ if (!sendQueue.isEmpty()) sendQueue.remove(0);
+ }
+ }, 0, 2);
+ }
+
+ /**
+ * Send a map to a player
+ *
+ * @param id0 The ID of the map
+ * @param content The {@link ArrayImage} to view on the map
+ * @param player The {@link Player} to view for
+ */
+ public static void sendMap(final int id0, final ArrayImage content, final Player player) {
+ if (player == null || !player.isOnline()) {
+ List toRemove = new ArrayList<>();
+ for (QueuedMap qMap : sendQueue) {
+ if (qMap == null) continue;
+
+ if (qMap.player == null || !qMap.player.isOnline()) {
+ toRemove.add(qMap);
+ }
+ }
+ Bukkit.getScheduler().cancelTask(senderID);
+ sendQueue.removeAll(toRemove);
+
+ return;
+ }
+
+ final MapId id = new MapId(-id0);
+ Bukkit.getScheduler().runTaskAsynchronously(MapReflectionAPI.getInstance(), () -> {
+ try {
+ WorldMap.b updateData = new WorldMap.b(
+ content.minX, //X pos
+ content.minY, //Y pos
+ content.maxX, //X size (2nd X pos)
+ content.maxY, //Y size (2nd Y pos)
+ content.array //Data
+ );
+
+ PacketPlayOutMap packet = new PacketPlayOutMap(
+ id, //ID
+ (byte) 0, //Scale
+ false, //Show icons
+ new ArrayList<>(), //Icons
+ updateData
+ );
+
+ ((CraftPlayer) player).getHandle().c.a(packet); //connection send()
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ record QueuedMap(int id, ArrayImage image, Player player) {
+ }
+}
diff --git a/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapWrapper_v1_21_R1.java b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapWrapper_v1_21_R1.java
new file mode 100644
index 0000000..bae1bea
--- /dev/null
+++ b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/MapWrapper_v1_21_R1.java
@@ -0,0 +1,243 @@
+/*
+ * This file is part of MapReflectionAPI.
+ * Copyright (c) 2022-2023 inventivetalent / SBDevelopment - All Rights Reserved
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package tech.sbdevelopment.mapreflectionapi.nms;
+
+import net.minecraft.core.component.DataComponentType;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata;
+import net.minecraft.network.protocol.game.PacketPlayOutSetSlot;
+import net.minecraft.network.syncher.DataWatcher;
+import net.minecraft.resources.MinecraftKey;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.decoration.EntityItemFrame;
+import net.minecraft.world.item.Items;
+import org.bukkit.*;
+import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
+import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
+import org.bukkit.entity.ItemFrame;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.metadata.FixedMetadataValue;
+import tech.sbdevelopment.mapreflectionapi.MapReflectionAPI;
+import tech.sbdevelopment.mapreflectionapi.api.ArrayImage;
+import tech.sbdevelopment.mapreflectionapi.api.MapController;
+import tech.sbdevelopment.mapreflectionapi.api.MapWrapper;
+import tech.sbdevelopment.mapreflectionapi.api.exceptions.MapLimitExceededException;
+
+import java.util.*;
+
+public class MapWrapper_v1_21_R1 extends MapWrapper {
+ protected MapController controller = new MapController() {
+ private final Map viewers = new HashMap<>();
+
+ @Override
+ public void addViewer(Player player) throws MapLimitExceededException {
+ if (!isViewing(player)) {
+ viewers.put(player.getUniqueId(), MapReflectionAPI.getMapManager().getNextFreeIdFor(player));
+ }
+ }
+
+ @Override
+ public void removeViewer(OfflinePlayer player) {
+ viewers.remove(player.getUniqueId());
+ }
+
+ @Override
+ public void clearViewers() {
+ for (UUID uuid : viewers.keySet()) {
+ viewers.remove(uuid);
+ }
+ }
+
+ @Override
+ public boolean isViewing(OfflinePlayer player) {
+ if (player == null) return false;
+ return viewers.containsKey(player.getUniqueId());
+ }
+
+ @Override
+ public int getMapId(OfflinePlayer player) {
+ if (isViewing(player)) {
+ return viewers.get(player.getUniqueId());
+ }
+ return -1;
+ }
+
+ @Override
+ public void update(ArrayImage content) {
+ MapWrapper duplicate = MapReflectionAPI.getMapManager().getDuplicate(content);
+ if (duplicate != null) {
+ MapWrapper_v1_21_R1.this.content = duplicate.getContent();
+ return;
+ }
+
+ MapWrapper_v1_21_R1.this.content = content;
+
+ for (UUID id : viewers.keySet()) {
+ sendContent(Bukkit.getPlayer(id));
+ }
+ }
+
+ @Override
+ public void sendContent(Player player) {
+ sendContent(player, false);
+ }
+
+ @Override
+ public void sendContent(Player player, boolean withoutQueue) {
+ if (!isViewing(player)) return;
+
+ int id = getMapId(player);
+ if (withoutQueue) {
+ MapSender_v1_21_R1.sendMap(id, MapWrapper_v1_21_R1.this.content, player);
+ } else {
+ MapSender_v1_21_R1.addToQueue(id, MapWrapper_v1_21_R1.this.content, player);
+ }
+ }
+
+ @Override
+ public void cancelSend() {
+ for (int s : viewers.values()) {
+ MapSender_v1_21_R1.cancelID(s);
+ }
+ }
+
+ @Override
+ public void showInInventory(Player player, int slot, boolean force) {
+ if (!isViewing(player)) return;
+
+ if (player.getGameMode() == GameMode.CREATIVE && !force) return;
+
+ if (slot < 9) {
+ slot += 36;
+ } else if (slot > 35 && slot != 45) {
+ slot = 8 - (slot - 36);
+ }
+
+ CraftPlayer craftPlayer = (CraftPlayer) player;
+ int windowId = craftPlayer.getHandle().cc.j; //inventoryMenu containerId
+ int stateId = craftPlayer.getHandle().cc.j(); //inventoryMenu getStateId()
+
+ ItemStack stack = new ItemStack(Material.FILLED_MAP, 1);
+ net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(stack);
+
+ PacketPlayOutSetSlot packet = new PacketPlayOutSetSlot(windowId, stateId, slot, nmsStack);
+ ((CraftPlayer) player).getHandle().c.a(packet);
+ }
+
+ @Override
+ public void showInInventory(Player player, int slot) {
+ showInInventory(player, slot, false);
+ }
+
+ @Override
+ public void showInHand(Player player, boolean force) {
+ if (player.getInventory().getItemInMainHand().getType() != Material.FILLED_MAP && !force) return;
+ showInInventory(player, player.getInventory().getHeldItemSlot(), force);
+ }
+
+ @Override
+ public void showInHand(Player player) {
+ showInHand(player, false);
+ }
+
+ @Override
+ public void showInFrame(Player player, ItemFrame frame) {
+ showInFrame(player, frame, false);
+ }
+
+ @Override
+ public void showInFrame(Player player, ItemFrame frame, boolean force) {
+ if (frame.getItem().getType() != Material.FILLED_MAP && !force) return;
+ showInFrame(player, frame.getEntityId());
+ }
+
+ @Override
+ public void showInFrame(Player player, int entityId) {
+ showInFrame(player, entityId, null);
+ }
+
+ @Override
+ public void showInFrame(Player player, int entityId, String debugInfo) {
+ if (!isViewing(player)) return;
+
+ ItemStack stack = new ItemStack(Material.FILLED_MAP, 1);
+ if (debugInfo != null) {
+ ItemMeta itemMeta = stack.getItemMeta();
+ itemMeta.setDisplayName(debugInfo);
+ stack.setItemMeta(itemMeta);
+ }
+
+ Bukkit.getScheduler().runTask(MapReflectionAPI.getInstance(), () -> {
+ ItemFrame frame = getItemFrameById(player.getWorld(), entityId);
+ if (frame != null) {
+ frame.removeMetadata("MAP_WRAPPER_REF", MapReflectionAPI.getInstance());
+ frame.setMetadata("MAP_WRAPPER_REF", new FixedMetadataValue(MapReflectionAPI.getInstance(), MapWrapper_v1_21_R1.this));
+ }
+
+ sendItemFramePacket(player, entityId, stack, getMapId(player));
+ });
+ }
+
+ @Override
+ public void clearFrame(Player player, int entityId) {
+
+ }
+
+ @Override
+ public void clearFrame(Player player, ItemFrame frame) {
+
+ }
+
+ @Override
+ public ItemFrame getItemFrameById(World world, int entityId) {
+ CraftWorld craftWorld = (CraftWorld) world;
+
+ Entity entity = craftWorld.getHandle().a(entityId);
+ if (entity == null) return null;
+
+ if (entity instanceof ItemFrame) return (ItemFrame) entity;
+
+ return null;
+ }
+
+ private void sendItemFramePacket(Player player, int entityId, ItemStack stack, int mapId) {
+ net.minecraft.world.item.ItemStack nmsStack = CraftItemStack.asNMSCopy(stack);
+ nmsStack.a().a(BuiltInRegistries.aq.a(MinecraftKey.a("minecraft:map_id")), mapId); //getOrCreateTag putInt
+
+ List> list = new ArrayList<>();
+ DataWatcher.c> dataWatcherItem = DataWatcher.c.a(EntityItemFrame.f, nmsStack);
+ list.add(dataWatcherItem);
+ PacketPlayOutEntityMetadata packet = new PacketPlayOutEntityMetadata(entityId, list);
+
+ ((CraftPlayer) player).getHandle().c.a(packet);
+ }
+ };
+
+ public MapWrapper_v1_21_R1(ArrayImage image) {
+ super(image);
+ }
+
+ @Override
+ public MapController getController() {
+ return controller;
+ }
+}
diff --git a/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/PacketListener_v1_21_R1.java b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/PacketListener_v1_21_R1.java
new file mode 100644
index 0000000..e722e1c
--- /dev/null
+++ b/NMS-v1_21_R1/src/main/java/tech/sbdevelopment/mapreflectionapi/nms/PacketListener_v1_21_R1.java
@@ -0,0 +1,126 @@
+/*
+ * This file is part of MapReflectionAPI.
+ * Copyright (c) 2022-2023 inventivetalent / SBDevelopment - All Rights Reserved
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package tech.sbdevelopment.mapreflectionapi.nms;
+
+import io.netty.channel.*;
+import net.minecraft.network.NetworkManager;
+import net.minecraft.network.protocol.game.PacketPlayInSetCreativeSlot;
+import net.minecraft.network.protocol.game.PacketPlayInUseEntity;
+import net.minecraft.network.protocol.game.PacketPlayOutMap;
+import net.minecraft.world.EnumHand;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.phys.Vec3D;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
+import org.bukkit.entity.Player;
+import org.bukkit.util.Vector;
+import tech.sbdevelopment.mapreflectionapi.MapReflectionAPI;
+import tech.sbdevelopment.mapreflectionapi.api.events.CreateInventoryMapUpdateEvent;
+import tech.sbdevelopment.mapreflectionapi.api.events.MapCancelEvent;
+import tech.sbdevelopment.mapreflectionapi.api.events.MapInteractEvent;
+import tech.sbdevelopment.mapreflectionapi.listeners.PacketListener;
+
+import java.util.concurrent.TimeUnit;
+
+import static tech.sbdevelopment.mapreflectionapi.utils.ReflectionUtil.*;
+
+public class PacketListener_v1_21_R1 extends PacketListener {
+ @Override
+ protected void injectPlayer(Player p) {
+ ChannelDuplexHandler channelDuplexHandler = new ChannelDuplexHandler() {
+ @Override
+ //On send packet
+ public void write(ChannelHandlerContext ctx, Object packet, ChannelPromise promise) throws Exception {
+ if (packet instanceof PacketPlayOutMap packetPlayOutMap) {
+ int id = (int) getDeclaredField(packetPlayOutMap, "a");
+
+ if (id < 0) {
+ //It's one of our maps, invert ID and let through!
+ int newId = -id;
+ setDeclaredField(packetPlayOutMap, "a", newId); //mapId
+ } else {
+ boolean async = !plugin.getServer().isPrimaryThread();
+ MapCancelEvent event = new MapCancelEvent(p, id, async);
+ if (MapReflectionAPI.getMapManager().isIdUsedBy(p, id)) event.setCancelled(true);
+ if (event.getHandlers().getRegisteredListeners().length > 0)
+ Bukkit.getPluginManager().callEvent(event);
+
+ if (event.isCancelled()) return;
+ }
+ }
+
+ super.write(ctx, packet, promise);
+ }
+
+ @Override
+ //On receive packet
+ public void channelRead(ChannelHandlerContext ctx, Object packet) throws Exception {
+ if (packet instanceof PacketPlayInUseEntity packetPlayInUseEntity) {
+ int entityId = (int) getDeclaredField(packetPlayInUseEntity, "a"); //entityId
+ Object action = getDeclaredField(packetPlayInUseEntity, "b"); //action
+ Enum> actionEnum = (Enum>) callDeclaredMethod(action, "a"); //action type
+ EnumHand hand = hasField(action, "a") ? (EnumHand) getDeclaredField(action, "a") : null; //hand
+ Vec3D pos = hasField(action, "b") ? (Vec3D) getDeclaredField(action, "b") : null; //pos
+
+ if (Bukkit.getScheduler().callSyncMethod(plugin, () -> {
+ boolean async = !plugin.getServer().isPrimaryThread();
+ MapInteractEvent event = new MapInteractEvent(p, entityId, actionEnum.ordinal(), pos != null ? vec3DToVector(pos) : null, hand != null ? hand.ordinal() : 0, async);
+ if (event.getFrame() != null && event.getMapWrapper() != null) {
+ Bukkit.getPluginManager().callEvent(event);
+ return event.isCancelled();
+ }
+ return false;
+ }).get(1, TimeUnit.SECONDS)) return;
+ } else if (packet instanceof PacketPlayInSetCreativeSlot packetPlayInSetCreativeSlot) {
+ int slot = packetPlayInSetCreativeSlot.b();
+ ItemStack item = packetPlayInSetCreativeSlot.e();
+
+ boolean async = !plugin.getServer().isPrimaryThread();
+ CreateInventoryMapUpdateEvent event = new CreateInventoryMapUpdateEvent(p, slot, CraftItemStack.asBukkitCopy(item), async);
+ if (event.getMapWrapper() != null) {
+ Bukkit.getPluginManager().callEvent(event);
+ if (event.isCancelled()) return;
+ }
+ }
+
+ super.channelRead(ctx, packet);
+ }
+ };
+
+ //The connection is private since 1.19.4 :|
+ NetworkManager networkManager = (NetworkManager) getField(((CraftPlayer) p).getHandle().c, "h");
+ ChannelPipeline pipeline = networkManager.n.pipeline(); //connection channel
+ pipeline.addBefore("packet_handler", p.getName(), channelDuplexHandler);
+ }
+
+ @Override
+ public void removePlayer(Player p) {
+ //The connection is private since 1.19.4 :|
+ NetworkManager networkManager = (NetworkManager) getField(((CraftPlayer) p).getHandle().c, "h");
+ Channel channel = networkManager.n; //connection channel
+ channel.eventLoop().submit(() -> channel.pipeline().remove(p.getName()));
+ }
+
+ @Override
+ protected Vector vec3DToVector(Object vec3d) {
+ if (!(vec3d instanceof Vec3D vec3dObj)) return new Vector(0, 0, 0);
+ return new Vector(vec3dObj.c, vec3dObj.d, vec3dObj.e); //x, y, z
+ }
+}
diff --git a/pom.xml b/pom.xml
index 1f55a66..1c36662 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,7 @@
API
Dist
+ NMS-v1_21_R1
NMS-v1_20_R3
NMS-v1_20_R2
NMS-v1_20_R1