diff --git a/src/main/java/space/essem/image2map/Image2Map.java b/src/main/java/space/essem/image2map/Image2Map.java index fadb14b..8079943 100644 --- a/src/main/java/space/essem/image2map/Image2Map.java +++ b/src/main/java/space/essem/image2map/Image2Map.java @@ -24,6 +24,7 @@ import net.minecraft.network.chat.HoverEvent; import net.minecraft.network.chat.Style; import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.permissions.PermissionLevel; import net.minecraft.util.Mth; import net.minecraft.world.InteractionHand; @@ -63,6 +64,7 @@ import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -462,6 +464,16 @@ public static boolean clickItemFrame(Player player, InteractionHand hand, ItemFr var entities = world.getEntitiesOfClass(ItemFrame.class, AABB.unitCubeFromLowerCorner(Vec3.atLowerCornerOf(mut)), (entity1) -> entity1.getDirection() == facing && entity1.blockPosition().equals(mut)); if (!entities.isEmpty()) { frames[x + y * width] = entities.get(0); + } else { + player.sendSystemMessage( + Component.literal( + String.format( + "Item frame wall is not large enough, expected %dx%d or larger", + width, height + ) + ) + ); + return true; } } } @@ -492,7 +504,27 @@ public static boolean clickItemFrame(Player player, InteractionHand hand, ItemFr return false; } - public static boolean destroyItemFrame(Entity player, ItemFrame itemFrameEntity) { + private static @Nullable ImageData getImageData(ItemStack item) { + var tag = item.get(DataComponents.CUSTOM_DATA); + if (tag == null) return null; + var codec = tag.copyTag().read(ImageData.CODEC); + return codec.orElse(null); + } + + private static @Nullable String getInputPathFromLore(ItemStack stack) { + if (getImageData(stack) == null) { + return null; + } + + var lore = stack.get(DataComponents.LORE); + if (lore == null || lore.lines().isEmpty()) { + return null; + } + + return lore.lines().getLast().getString(); + } + + public static boolean destroyItemFrame(ServerLevel serverLevel, Entity player, ItemFrame itemFrameEntity) { var stack = itemFrameEntity.getItem(); var tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag().read(ImageData.CODEC); @@ -509,6 +541,7 @@ public static boolean destroyItemFrame(Entity player, ItemFrame itemFrameEntity) Direction facing = tag.orElseThrow().facing().get(); var world = itemFrameEntity.level(); + var itemFramePosition = itemFrameEntity.blockPosition(); var start = itemFrameEntity.blockPosition(); var mut = start.mutable(); @@ -518,22 +551,33 @@ public static boolean destroyItemFrame(Entity player, ItemFrame itemFrameEntity) start = mut.immutable(); - for (var x = 0; x < width; x++) { - for (var y = 0; y < height; y++) { + ArrayList frameItems = new ArrayList<>(); + + for (var y = 0; y < height; y++) { + for (var x = 0; x < width; x++) { mut.set(start); mut.move(right, x); mut.move(down, y); var entities = world.getEntitiesOfClass(ItemFrame.class, AABB.unitCubeFromLowerCorner(Vec3.atLowerCornerOf(mut)), (entity1) -> entity1.getDirection() == facing && entity1.blockPosition().equals(mut)); + + // Fix for the item frame technically not existing in the world + // after the block holding it has been destroyed + ItemFrame frame = null; if (!entities.isEmpty()) { - var frame = entities.get(0); + frame = entities.getFirst(); + } else if (itemFramePosition.equals(mut)) { + frame = itemFrameEntity; + } + if (frame != null) { // Only apply to frames that contain an image2map map var frameStack = frame.getItem(); tag = frameStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag().read(ImageData.CODEC); if (frameStack.getItem() == Items.FILLED_MAP && tag.isPresent() && tag.orElseThrow().right().isPresent() && tag.orElseThrow().down().isPresent() && tag.orElseThrow().facing().isPresent()) { + frameItems.add(frameStack); frame.setItem(ItemStack.EMPTY, true); frame.setInvisible(false); } @@ -541,12 +585,114 @@ public static boolean destroyItemFrame(Entity player, ItemFrame itemFrameEntity) } } + if (!frameItems.isEmpty()) { + String inputPath = getInputPathFromLore(frameItems.getFirst()); + if (inputPath == null) { + inputPath = "unknown"; + } + + ArrayList frameItemTemplates = new ArrayList<>(); + // Clear the right/down/facing data from the items, + // so they don't get batch removed if placed individually later. + for (ItemStack item : frameItems) { + var customData = item.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .copyTag() + .read(ImageData.CODEC) + .get(); + item.set( + DataComponents.CUSTOM_DATA, + CustomData.of( + ImageData.CODEC.codec().encodeStart( + NbtOps.INSTANCE, + new ImageData( + customData.x(), + customData.y(), + customData.width(), + customData.height(), + customData.quickPlace(), + Optional.empty(), + Optional.empty(), + Optional.empty() + ) + ).result().orElseThrow().asCompound().orElseThrow() + ) + ); + + frameItemTemplates.add(ItemStackTemplate.fromNonEmptyStack(item)); + } + + itemFrameEntity.spawnAtLocation(serverLevel, toSingleStack(frameItemTemplates, inputPath, width * 128, height * 128).create()); + } + return true; } return false; } + public static void destroyBundleOnEmpty(ItemStack bundle) { + if (getImageData(bundle) == null) { + return; + } + + var contents = bundle.get(DataComponents.BUNDLE_CONTENTS); + if (contents == null || contents.isEmpty()) { + bundle.shrink(1); + } + } + + public static boolean isInvalidMapForBundle(ItemStack bundle, ItemStack item) { + if (item.is(Items.AIR)) { + return false; + } + + var bundleData = getImageData(bundle); + + // Allow insert if bundle isn't an image2map bundle + if (bundleData == null) { + return false; + } + + // Block insert if item isn't a map + if (!item.is(Items.FILLED_MAP)) { + return true; + } + + var mapData = getImageData(item); + + // Block insert if map isn't an image2map map + if (mapData == null) { + return true; + } + + var bundleInputPath = getInputPathFromLore(bundle); + var mapInputPath = getInputPathFromLore(item); + + // Block insert if there's either no input for either of the items, or they don't match + if (bundleInputPath == null || !bundleInputPath.equals(mapInputPath)) { + return true; + } + + var bundleMaps = bundle.get(DataComponents.BUNDLE_CONTENTS); + + // Potential edge case for empty image2map bundle? Best to check either way. + // Allow insert if bundle is empty. + if (bundleMaps == null) { + return false; + } + + // Block insert if the bundle already contains a map with the same tiling coordinates. + for (var map : bundleMaps.items()) { + var data = getImageData(map.create()); + if (data == null) continue; + if (data.x() == mapData.x() && data.y() == mapData.y()) { + return true; + } + } + + return false; + } + private static boolean isValid(String url) { try { new URL(url).toURI(); diff --git a/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java b/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java index 2cc3683..5ec76af 100644 --- a/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java +++ b/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java @@ -1,8 +1,5 @@ package space.essem.image2map.mixin; -import net.minecraft.core.component.DataComponents; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.SlotAccess; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.ClickAction; @@ -13,43 +10,120 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import space.essem.image2map.Image2Map; + @Mixin(BundleItem.class) public class BundleItemMixin { + /** + * When holding a bundle in your hand in-world, + * and right-clicking to drop an item, destroy the bundle if it ends up empty. + * Overridden if the player is in creative mode. + */ + @Inject(method = "dropContent(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/item/ItemStack;)V", at = @At("RETURN")) + private void image2map$dropContent(Level level, Player player, ItemStack stack, CallbackInfo ci) { + if (player.isCreative()) { + return; + } + + Image2Map.destroyBundleOnEmpty(stack); + } + + /** + * When holding a bundle with the mouse in the inventory, + * and left-clicking a slot with another item, only place + * the item into the bundle if it's a valid image2map map for the bundle. + * Overridden if the player is in creative mode. + */ + @Inject(method = "overrideStackedOnOther", at = @At("HEAD"), cancellable = true) + private void image2map$overrideStackedOnOtherHead(ItemStack itemStack, Slot slot, ClickAction clickAction, Player player, CallbackInfoReturnable cir) { + if (player.isCreative()) { + return; + } + + if (clickAction != ClickAction.PRIMARY) { + return; + } - @Inject(method = "use", at = @At("HEAD"), cancellable = true) - private void image2map$useBundle(Level world, Player user, InteractionHand hand, - CallbackInfoReturnable cir) { - ItemStack itemStack = user.getItemInHand(hand); - var tag = itemStack.get(DataComponents.CUSTOM_DATA); + if (Image2Map.isInvalidMapForBundle(itemStack, slot.getItem())) { + cir.setReturnValue(false); + } + } + + /** + * When holding a bundle with the mouse in the inventory, + * and right-clicking a slot with no item, + * destroy the bundle if it ends up empty. + * Overridden if the player is in creative mode. + */ + @Inject(method = "overrideStackedOnOther", at = @At("RETURN")) + private void image2map$overrideStackedOnOtherReturn(ItemStack itemStack, Slot slot, ClickAction clickAction, Player player, CallbackInfoReturnable cir) { + if (player.isCreative()) { + return; + } + + if (clickAction != ClickAction.SECONDARY) { + return; + } - if (tag != null && tag.copyTag().contains("image2map:quick_place") && !user.isCreative()) { - cir.setReturnValue(InteractionResult.FAIL); - cir.cancel(); + Image2Map.destroyBundleOnEmpty(itemStack); } - } - @Inject(method = "overrideStackedOnOther", at = @At("HEAD"), cancellable = true) - private void image2map$addBundleItems(ItemStack bundle, Slot slot, ClickAction clickType, Player player, - CallbackInfoReturnable cir) { - var tag = bundle.get(DataComponents.CUSTOM_DATA); + /** + * When holding an item with the mouse in the inventory, + * and left-clicking a slot with a bundle, only place + * the item into the bundle if it's a valid image2map map for the bundle. + * Overridden if the player is in creative mode. + */ + @Inject(method = "overrideOtherStackedOnMe", at = @At("HEAD"), cancellable = true) + private void image2map$overrideOtherStackedOnMeHead( + ItemStack bundle, + ItemStack otherStack, + Slot slot, + ClickAction clickAction, + Player player, + SlotAccess slotAccess, + CallbackInfoReturnable cir + ) { + if (player.isCreative()) { + return; + } - if (tag != null && tag.copyTag().contains("image2map:quick_place") && !player.isCreative()) { - cir.setReturnValue(false); - cir.cancel(); + if (clickAction != ClickAction.PRIMARY) { + return; + } + + if (Image2Map.isInvalidMapForBundle(bundle, otherStack)) { + cir.setReturnValue(false); + } } - } - @Inject(method = "overrideOtherStackedOnMe", at = @At("HEAD"), cancellable = true) - private void image2map$removeBundleItems(ItemStack bundle, ItemStack otherStack, Slot slot, ClickAction clickType, - Player player, SlotAccess cursorStackReference, - CallbackInfoReturnable cir) { - var tag = bundle.get(DataComponents.CUSTOM_DATA); + /** + * When holding no item with the mouse in the inventory, + * and right-clicking a slot with a bundle, + * destroy the bundle if it ends up empty. + * Overridden if the player is in creative mode. + */ + @Inject(method = "overrideOtherStackedOnMe", at = @At("RETURN")) + private void image2map$overrideOtherStackedOnMeReturn( + ItemStack bundle, + ItemStack otherStack, + Slot slot, + ClickAction clickAction, + Player player, + SlotAccess slotAccess, + CallbackInfoReturnable cir + ) { + if (player.isCreative()) { + return; + } + + if (clickAction != ClickAction.SECONDARY) { + return; + } - if (tag != null && tag.copyTag().contains("image2map:quick_place") && !player.isCreative()) { - cir.setReturnValue(false); - cir.cancel(); + Image2Map.destroyBundleOnEmpty(bundle); } - } } diff --git a/src/main/java/space/essem/image2map/mixin/ItemFrameMixin.java b/src/main/java/space/essem/image2map/mixin/ItemFrameMixin.java index 41edec7..7d01044 100644 --- a/src/main/java/space/essem/image2map/mixin/ItemFrameMixin.java +++ b/src/main/java/space/essem/image2map/mixin/ItemFrameMixin.java @@ -33,7 +33,7 @@ public class ItemFrameMixin { private void image2map$destroyMaps(ServerLevel world, Entity entity, boolean dropSelf, CallbackInfo ci) { var frame = (ItemFrame) (Object) this; - if (!this.fixed && Image2Map.destroyItemFrame(entity, frame)) { + if (!this.fixed && Image2Map.destroyItemFrame(world, entity, frame)) { if (dropSelf) { frame.spawnAtLocation(world, new ItemStack(Items.ITEM_FRAME)); }