diff --git a/.gitignore b/.gitignore index 27d5dde..57f8bd9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # IntelliJ out/ +# vscode +.vscode + # Compiled class file *.class diff --git a/V1_21_11/pom.xml b/V1_21_11/pom.xml new file mode 100644 index 0000000..ef01652 --- /dev/null +++ b/V1_21_11/pom.xml @@ -0,0 +1,93 @@ + + + + + 4.0.0 + + + com.loohp + ImageFrame-Parent + 1.8.7.5 + + + ImageFrame-V1_21_11 + + + + org.spigotmc + spigot-api + 1.21.11-R0.1-SNAPSHOT + provided + + + org.bukkit + craftbukkit + 1.21.11-R0.1-SNAPSHOT + provided + + + net.kyori + adventure-text-serializer-gson + 4.25.0 + provided + + + net.kyori + adventure-text-serializer-legacy + 4.25.0 + provided + + + net.kyori + adventure-text-serializer-plain + 4.25.0 + provided + + + net.kyori + adventure-api + 4.25.0 + provided + + + com.loohp + ImageFrame-Abstraction + ${project.parent.version} + provided + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + + + + + diff --git a/V1_21_11/src/main/java/com/loohp/imageframe/nms/V1_21_11.java b/V1_21_11/src/main/java/com/loohp/imageframe/nms/V1_21_11.java new file mode 100644 index 0000000..33bf88d --- /dev/null +++ b/V1_21_11/src/main/java/com/loohp/imageframe/nms/V1_21_11.java @@ -0,0 +1,390 @@ +/* + * This file is part of ImageFrame. + * + * Copyright (C) 2025. LoohpJames + * Copyright (C) 2025. Contributors + * + * 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 com.loohp.imageframe.nms; + +import com.google.common.collect.Collections2; +import com.loohp.imageframe.objectholders.CombinedMapItemInfo; +import com.loohp.imageframe.objectholders.FilledMapItemInfo; +import com.loohp.imageframe.objectholders.MutablePair; +import com.loohp.imageframe.utils.ReflectionUtils; +import com.loohp.imageframe.utils.UUIDUtils; +import net.kyori.adventure.key.Key; +import net.minecraft.EnumChatFormat; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.network.chat.ChatModifier; +import net.minecraft.network.chat.IChatBaseComponent; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.PacketPlayOutEntityMetadata; +import net.minecraft.network.protocol.game.PacketPlayOutMap; +import net.minecraft.network.syncher.DataWatcher; +import net.minecraft.network.syncher.DataWatcherObject; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.EntityPlayer; +import net.minecraft.server.level.WorldServer; +import net.minecraft.world.entity.decoration.EntityItemFrame; +import net.minecraft.world.entity.player.EntityHuman; +import net.minecraft.world.entity.player.PlayerInventory; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.ItemLore; +import net.minecraft.world.level.saveddata.maps.MapDecorationType; +import net.minecraft.world.level.saveddata.maps.MapIcon; +import net.minecraft.world.level.saveddata.maps.MapId; +import net.minecraft.world.level.saveddata.maps.PersistentIdCounts; +import net.minecraft.world.level.saveddata.maps.WorldMap; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_21_R7.CraftWorld; +import org.bukkit.craftbukkit.v1_21_R7.entity.CraftEntity; +import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack; +import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapCursor; +import org.bukkit.craftbukkit.v1_21_R7.map.CraftMapView; +import org.bukkit.craftbukkit.v1_21_R7.map.RenderData; +import org.bukkit.craftbukkit.v1_21_R7.util.CraftChatMessage; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.map.MapCursor; +import org.bukkit.map.MapView; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class V1_21_11 extends NMSWrapper { + + private final Field nmsEntityByteDataWatcherField; + private final Field craftMapViewWorldMapField; + private final Field persistentIdCountsLastMapIdField; + private final Field renderDataCursorsField; + + public V1_21_11() { + try { + nmsEntityByteDataWatcherField = ReflectionUtils.findDeclaredField(net.minecraft.world.entity.Entity.class, DataWatcherObject.class, "DATA_SHARED_FLAGS_ID", "aA"); + craftMapViewWorldMapField = CraftMapView.class.getDeclaredField("worldMap"); + Field persistentIdCountsLastMapIdField0; + try { + persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, int.class, "lastMapId", "d"); + } catch (NoSuchFieldException e) { + persistentIdCountsLastMapIdField0 = ReflectionUtils.findDeclaredField(PersistentIdCounts.class, AtomicInteger.class, "lastMapId", "d"); + } + persistentIdCountsLastMapIdField = persistentIdCountsLastMapIdField0; + renderDataCursorsField = RenderData.class.getField("cursors"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + public WorldMap getWorldMap(MapView mapView) { + try { + CraftMapView craftMapView = (CraftMapView) mapView; + craftMapViewWorldMapField.setAccessible(true); + return (WorldMap) craftMapViewWorldMapField.get(craftMapView); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public void setColors(MapView mapView, byte[] colors) { + if (colors.length != COLOR_ARRAY_LENGTH) { + throw new IllegalArgumentException("colors array length must be " + COLOR_ARRAY_LENGTH); + } + WorldMap nmsWorldMap = getWorldMap(mapView); + nmsWorldMap.h = colors; + } + + @Override + public Collection getViewers(MapView mapView) { + WorldMap nmsWorldMap = getWorldMap(mapView); + Map humansMap = nmsWorldMap.q; + return Collections2.transform(humansMap.keySet(), e -> (Player) e.getBukkitEntity()); + } + + @Override + public boolean hasViewers(MapView mapView) { + WorldMap nmsWorldMap = getWorldMap(mapView); + Map humansMap = nmsWorldMap.q; + return !humansMap.isEmpty(); + } + + @Override + public MapIcon toNMSMapIcon(MapCursor mapCursor) { + Holder decorationTypeHolder = toNMSMapIconType(mapCursor.getType()); + IChatBaseComponent iChat = CraftChatMessage.fromStringOrNull(mapCursor.getCaption()); + return new MapIcon(decorationTypeHolder, mapCursor.getX(), mapCursor.getY(), mapCursor.getDirection(), Optional.ofNullable(iChat)); + } + + @Override + public Holder toNMSMapIconType(MapCursor.Type type) { + return CraftMapCursor.CraftType.bukkitToMinecraftHolder(type); + } + + @Override + public boolean isRenderOnFrame(MapCursor.Type type) { + Holder decorationTypeHolder = toNMSMapIconType(type); + return decorationTypeHolder.a().c(); + } + + @Override + public int getNextAvailableMapId(World world) { + try { + persistentIdCountsLastMapIdField.setAccessible(true); + WorldServer worldServer = ((CraftWorld) world).getHandle(); + PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b); + if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) { + AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts); + return atomicInteger.get() + 1; + } else { + return persistentIdCountsLastMapIdField.getInt(persistentIdCounts) + 1; + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("deprecation") + @Override + public MapView getMapOrCreateMissing(World world, int id) { + try { + MapView mapView = Bukkit.getMap(id); + if (mapView != null) { + return mapView; + } + persistentIdCountsLastMapIdField.setAccessible(true); + Location spawnLocation = world.getSpawnLocation(); + WorldServer worldServer = ((CraftWorld) world).getHandle(); + ResourceKey worldTypeKey = worldServer.aq(); + WorldMap worldMap = WorldMap.a(spawnLocation.getX(), spawnLocation.getZ(), (byte) 3, false, false, worldTypeKey); + MapId mapId = new MapId(id); + worldServer.a(mapId, worldMap); + PersistentIdCounts persistentIdCounts = worldServer.s().N().A().a(PersistentIdCounts.b); + if (persistentIdCountsLastMapIdField.getType().equals(AtomicInteger.class)) { + AtomicInteger atomicInteger = (AtomicInteger) persistentIdCountsLastMapIdField.get(persistentIdCounts); + atomicInteger.getAndUpdate(lastMapId -> Math.max(lastMapId, id)); + } else { + int lastMapId = persistentIdCountsLastMapIdField.getInt(persistentIdCounts); + persistentIdCountsLastMapIdField.setInt(persistentIdCounts, Math.max(lastMapId, id)); + } + persistentIdCounts.u(); + return Bukkit.getMap(id); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("unchecked") + @Override + public MutablePair> bukkitRenderMap(MapView mapView, Player player) { + try { + CraftMapView craftMapView = (CraftMapView) mapView; + CraftPlayer craftPlayer = (CraftPlayer) player; + RenderData renderData = craftMapView.render(craftPlayer); + return new MutablePair<>(renderData.buffer, (List) renderDataCursorsField.get(renderData)); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Override + public Set getEntityTrackers(Entity entity) { + return new HashSet<>(entity.getTrackedBy()); + } + + @Override + public PacketPlayOutMap createMapPacket(int mapId, byte[] colors, Collection cursors) { + List mapIcons = cursors == null ? null : cursors.stream().map(this::toNMSMapIcon).collect(Collectors.toList()); + WorldMap.c c = colors == null ? null : new WorldMap.c(0, 0, 128, 128, colors); + return new PacketPlayOutMap(new MapId(mapId), (byte) 0, false, Optional.ofNullable(mapIcons), Optional.ofNullable(c)); + } + + @Override + public PacketPlayOutEntityMetadata createItemFrameItemChangePacket(int entityId, ItemStack itemStack) { + List> dataWatchers = Collections.singletonList(DataWatcher.c.a(EntityItemFrame.c, CraftItemStack.asNMSCopy(itemStack))); + return new PacketPlayOutEntityMetadata(entityId, dataWatchers); + } + + @SuppressWarnings("unchecked") + @Override + public Object createEntityFlagsPacket(Entity entity, Boolean invisible, Boolean glowing) { + try { + int entityId = entity.getEntityId(); + net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle(); + + nmsEntityByteDataWatcherField.setAccessible(true); + + DataWatcher watcher = nmsEntity.aD(); + DataWatcherObject byteField = (DataWatcherObject) nmsEntityByteDataWatcherField.get(null); + byte value = watcher.a(byteField); + + if (invisible != null) { + if (invisible) { + value = (byte) (value | 0x20); + } else { + value = (byte) (value & ~0x20); + } + } + if (glowing != null) { + if (glowing) { + value = (byte) (value | 0x40); + } else { + value = (byte) (value & ~0x40); + } + } + + List> dataWatchers = Collections.singletonList(DataWatcher.c.a(byteField, value)); + return new PacketPlayOutEntityMetadata(entityId, dataWatchers); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void sendPacket(Player player, Object packet) { + ((CraftPlayer) player).getHandle().g.b((Packet) packet); + } + + @SuppressWarnings("OptionalGetWithoutIsPresent") + @Override + public CombinedMapItemInfo getCombinedMapItemInfo(ItemStack itemStack) { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + CustomData customData = nmsItemStack.a(DataComponents.b); + if (customData == null) { + return null; + } + NBTTagCompound tag = customData.b(); + if (!tag.b(CombinedMapItemInfo.KEY)) { + return null; + } + int imageMapIndex = tag.b(CombinedMapItemInfo.KEY, -1); + if (!tag.b(CombinedMapItemInfo.PLACEMENT_UUID_KEY) || !tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY)) { + return new CombinedMapItemInfo(imageMapIndex); + } + float yaw = tag.b(CombinedMapItemInfo.PLACEMENT_YAW_KEY, 0F); + UUID uuid = UUIDUtils.fromIntArray(tag.k(CombinedMapItemInfo.PLACEMENT_UUID_KEY).get()); + return new CombinedMapItemInfo(imageMapIndex, new CombinedMapItemInfo.PlacementInfo(yaw, uuid)); + } + + @Override + public ItemStack withCombinedMapItemInfo(ItemStack itemStack, CombinedMapItemInfo combinedMapItemInfo) { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a); + NBTTagCompound tag = customData.b(); + tag.a(CombinedMapItemInfo.KEY, combinedMapItemInfo.getImageMapIndex()); + if (combinedMapItemInfo.hasPlacement()) { + CombinedMapItemInfo.PlacementInfo placement = combinedMapItemInfo.getPlacement(); + tag.a(CombinedMapItemInfo.PLACEMENT_YAW_KEY, placement.getYaw()); + tag.a(CombinedMapItemInfo.PLACEMENT_UUID_KEY, UUIDUtils.toIntArray(placement.getUniqueId())); + } + nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a()); + return CraftItemStack.asCraftMirror(nmsItemStack); + } + + @Override + public FilledMapItemInfo getFilledMapItemInfo(ItemStack itemStack) { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + CustomData customData = nmsItemStack.a(DataComponents.b); + if (customData == null) { + return null; + } + NBTTagCompound tag = customData.b(); + if (!tag.b(FilledMapItemInfo.KEY)) { + return null; + } + int imageMapIndex = tag.b(FilledMapItemInfo.KEY, -1); + int mapPartIndex = tag.b(FilledMapItemInfo.INDEX_KEY, -1); + return new FilledMapItemInfo(imageMapIndex, mapPartIndex); + } + + @Override + public ItemStack withFilledMapItemInfo(ItemStack itemStack, FilledMapItemInfo filledMapItemInfo) { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + CustomData customData = nmsItemStack.a(DataComponents.b, CustomData.a); + NBTTagCompound tag = customData.b(); + tag.a(FilledMapItemInfo.KEY, filledMapItemInfo.getImageMapIndex()); + tag.a(FilledMapItemInfo.INDEX_KEY, filledMapItemInfo.getMapPartIndex()); + nmsItemStack.b(DataComponentPatch.a().a(DataComponents.b, CustomData.a(tag)).a()); + return CraftItemStack.asCraftMirror(nmsItemStack); + } + + @Override + public ItemStack withInvisibleItemFrameMeta(ItemStack itemStack) { + if (itemStack == null || itemStack.getType().equals(Material.AIR)) { + return itemStack; + } + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + ItemLore itemLore = nmsItemStack.a(DataComponents.m, ItemLore.a); + List loreLines = new ArrayList<>(itemLore.a()); + loreLines.add(0, IChatBaseComponent.c("effect.minecraft.invisibility").c(ChatModifier.a.c(EnumChatFormat.h).b(false))); + nmsItemStack.b(DataComponentPatch.a().a(DataComponents.m, new ItemLore(loreLines)).a()); + ItemStack modified = CraftItemStack.asCraftMirror(nmsItemStack); + ItemMeta itemMeta = modified.getItemMeta(); + if (itemMeta == null) { + return itemStack; + } + itemMeta.addEnchant(Enchantment.LUCK_OF_THE_SEA, 1, true); + itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + modified.setItemMeta(itemMeta); + return modified; + } + + @Override + public List giveItems(Player player, List itemStacks) { + List leftovers = new ArrayList<>(); + EntityPlayer nmsPlayer = ((CraftPlayer) player).getHandle(); + PlayerInventory inventory = nmsPlayer.gK(); + for (ItemStack itemStack : itemStacks) { + net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); + boolean added = inventory.g(nmsItemStack); + if (!added || !nmsItemStack.f()) { + leftovers.add(CraftItemStack.asBukkitCopy(nmsItemStack)); + } + } + nmsPlayer.cn.d(); + return leftovers; + } + + @SuppressWarnings("PatternValidation") + public Key getWorldNamespacedKey(World world) { + NamespacedKey key = world.getKey(); + return Key.key(key.getNamespace(), key.getKey()); + } + +} diff --git a/common/pom.xml b/common/pom.xml index 025260d..08a8ef1 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -503,5 +503,11 @@ ${project.version} compile + + com.loohp + ImageFrame-V1_21_11 + ${project.version} + compile + diff --git a/common/src/main/java/com/loohp/imageframe/utils/MCVersion.java b/common/src/main/java/com/loohp/imageframe/utils/MCVersion.java index a7c86b3..168405e 100644 --- a/common/src/main/java/com/loohp/imageframe/utils/MCVersion.java +++ b/common/src/main/java/com/loohp/imageframe/utils/MCVersion.java @@ -28,6 +28,7 @@ public enum MCVersion { + V1_21_11("1.21.11", "1_21_R7", 37), V1_21_10("1.21.10", "1_21_R6", 36), V1_21_9("1.21.9", "1_21_R6", 35), V1_21_8("1.21.8", "1_21_R5", 34),