From 35fa58fec9cfaee4d0636c2b78c83c37b7eaee16 Mon Sep 17 00:00:00 2001 From: Leonard Joensen Date: Mon, 28 Jul 2025 07:53:41 +0200 Subject: [PATCH 1/4] Block incorrect map bundle placements --- src/main/java/space/essem/image2map/Image2Map.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/space/essem/image2map/Image2Map.java b/src/main/java/space/essem/image2map/Image2Map.java index fc2f88d..f0ab437 100644 --- a/src/main/java/space/essem/image2map/Image2Map.java +++ b/src/main/java/space/essem/image2map/Image2Map.java @@ -431,6 +431,17 @@ public static boolean clickItemFrame(PlayerEntity player, Hand hand, ItemFrameEn var entities = world.getEntitiesByClass(ItemFrameEntity.class, Box.from(Vec3d.of(mut)), (entity1) -> entity1.getHorizontalFacing() == facing && entity1.getBlockPos().equals(mut)); if (!entities.isEmpty()) { frames[x + y * width] = entities.get(0); + } else { + player.sendMessage( + Text.literal( + String.format( + "Item frame wall is not large enough, expected %dx%d or larger", + width, height + ) + ), + false + ); + return true; } } } From 64c1d32e4a127ec542e21eff44d9ab3e1f70dc19 Mon Sep 17 00:00:00 2001 From: Leonard Joensen Date: Mon, 28 Jul 2025 09:05:50 +0200 Subject: [PATCH 2/4] Drop maps when map wall is destroyed --- .../java/space/essem/image2map/Image2Map.java | 94 ++++++++++++++++--- .../image2map/mixin/ItemFrameEntityMixin.java | 4 +- 2 files changed, 82 insertions(+), 16 deletions(-) diff --git a/src/main/java/space/essem/image2map/Image2Map.java b/src/main/java/space/essem/image2map/Image2Map.java index f0ab437..9b9f230 100644 --- a/src/main/java/space/essem/image2map/Image2Map.java +++ b/src/main/java/space/essem/image2map/Image2Map.java @@ -20,26 +20,22 @@ import net.minecraft.component.type.LoreComponent; import net.minecraft.component.type.NbtComponent; import net.minecraft.entity.Entity; -import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.decoration.ItemFrameEntity; import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.BundleItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; -import net.minecraft.nbt.*; +import net.minecraft.nbt.NbtOps; import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.world.ServerWorld; import net.minecraft.text.HoverEvent; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Hand; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.BlockPos.Mutable; import net.minecraft.util.math.Box; import net.minecraft.util.math.Direction; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; -import net.minecraft.world.World; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import space.essem.image2map.config.Image2MapConfig; @@ -48,11 +44,9 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URL; -import java.net.URLConnection; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -62,9 +56,9 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.time.Duration; -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; @@ -472,7 +466,37 @@ public static boolean clickItemFrame(PlayerEntity player, Hand hand, ItemFrameEn return false; } - public static boolean destroyItemFrame(Entity player, ItemFrameEntity itemFrameEntity) { + private static @Nullable ImageData getImageData(ItemStack item) { + var tag = item.get(DataComponentTypes.CUSTOM_DATA); + if (tag == null) return null; + var codec = tag.get(ImageData.CODEC); + return codec.isSuccess() ? codec.getOrThrow() : null; + } + + private static @Nullable String getUrlFromLore(ItemStack stack) { + if (getImageData(stack) == null) { + return null; + } + + var lore = stack.get(DataComponentTypes.LORE); + if (lore == null || lore.lines().isEmpty()) { + return null; + } + + // This could be simplified to lore.lines().getLast().getString() currently, + // however doing it this way ensures future compatibility with any + // added lore elements. + for (var line : lore.lines()) { + var lineString = line.getString(); + if (lineString.startsWith("http://") || lineString.startsWith("https://")) { + return lineString; + } + } + + return null; + } + + public static boolean destroyItemFrame(ServerWorld serverWorld, Entity player, ItemFrameEntity itemFrameEntity) { var stack = itemFrameEntity.getHeldItemStack(); var tag = stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); @@ -489,6 +513,7 @@ public static boolean destroyItemFrame(Entity player, ItemFrameEntity itemFrameE Direction facing = tag.getOrThrow().facing().get(); var world = itemFrameEntity.getWorld(); + var itemFramePosition = itemFrameEntity.getBlockPos(); var start = itemFrameEntity.getBlockPos(); var mut = start.mutableCopy(); @@ -498,22 +523,33 @@ public static boolean destroyItemFrame(Entity player, ItemFrameEntity itemFrameE start = mut.toImmutable(); - 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.getEntitiesByClass(ItemFrameEntity.class, Box.from(Vec3d.of(mut)), (entity1) -> entity1.getHorizontalFacing() == facing && entity1.getBlockPos().equals(mut)); + + // Fix for the item frame technically not existing in the world + // after the block holding it has been destroyed + ItemFrameEntity 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.getHeldItemStack(); tag = frameStack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); if (frameStack.getItem() == Items.FILLED_MAP && tag.isSuccess() && tag.getOrThrow().right().isPresent() && tag.getOrThrow().down().isPresent() && tag.getOrThrow().facing().isPresent()) { + frameItems.add(frameStack); frame.setHeldItemStack(ItemStack.EMPTY, true); frame.setInvisible(false); } @@ -521,6 +557,38 @@ public static boolean destroyItemFrame(Entity player, ItemFrameEntity itemFrameE } } + if (!frameItems.isEmpty()) { + String url = getUrlFromLore(frameItems.getFirst()); + if (url == null) { + url = "unknown"; + } + + // 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(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC).getOrThrow(); + item.set( + DataComponentTypes.CUSTOM_DATA, + NbtComponent.DEFAULT.with( + NbtOps.INSTANCE, + ImageData.CODEC, + new ImageData( + customData.x(), + customData.y(), + customData.width(), + customData.height(), + customData.quickPlace(), + Optional.empty(), + Optional.empty(), + Optional.empty() + ) + ).getOrThrow() + ); + } + + itemFrameEntity.dropStack(serverWorld, toSingleStack(frameItems, url, width * 128, height * 128)); + } + return true; } diff --git a/src/main/java/space/essem/image2map/mixin/ItemFrameEntityMixin.java b/src/main/java/space/essem/image2map/mixin/ItemFrameEntityMixin.java index 7935d88..567d4dd 100644 --- a/src/main/java/space/essem/image2map/mixin/ItemFrameEntityMixin.java +++ b/src/main/java/space/essem/image2map/mixin/ItemFrameEntityMixin.java @@ -8,8 +8,6 @@ import net.minecraft.server.world.ServerWorld; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; - -import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -34,7 +32,7 @@ public class ItemFrameEntityMixin { private void image2map$destroyMaps(ServerWorld world, Entity entity, boolean dropSelf, CallbackInfo ci) { var frame = (ItemFrameEntity) (Object) this; - if (!this.fixed && Image2Map.destroyItemFrame(entity, frame)) { + if (!this.fixed && Image2Map.destroyItemFrame(world, entity, frame)) { if (dropSelf) { frame.dropStack(world, new ItemStack(Items.ITEM_FRAME)); } From 294bb29ef586863c7872b68d4149285cbd91ae07 Mon Sep 17 00:00:00 2001 From: Leonard Joensen Date: Mon, 28 Jul 2025 09:30:35 +0200 Subject: [PATCH 3/4] Allow adding and removing maps to image2map bundles --- .../java/space/essem/image2map/Image2Map.java | 63 +++++++++ .../image2map/mixin/BundleItemMixin.java | 131 ++++++++++++++---- 2 files changed, 165 insertions(+), 29 deletions(-) diff --git a/src/main/java/space/essem/image2map/Image2Map.java b/src/main/java/space/essem/image2map/Image2Map.java index 9b9f230..164f13f 100644 --- a/src/main/java/space/essem/image2map/Image2Map.java +++ b/src/main/java/space/essem/image2map/Image2Map.java @@ -595,6 +595,69 @@ public static boolean destroyItemFrame(ServerWorld serverWorld, Entity player, I return false; } + public static void destroyBundleOnEmpty(ItemStack bundle) { + if (getImageData(bundle) == null) { + return; + } + + var contents = bundle.get(DataComponentTypes.BUNDLE_CONTENTS); + if (contents == null || contents.isEmpty()) { + bundle.decrement(1); + } + } + + public static boolean isInvalidMapForBundle(ItemStack bundle, ItemStack item) { + if (item.isOf(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.isOf(Items.FILLED_MAP)) { + return true; + } + + var mapData = getImageData(item); + + // Block insert if map isn't an image2map map + if (mapData == null) { + return true; + } + + var bundleUrl = getUrlFromLore(bundle); + var mapUrl = getUrlFromLore(item); + + // Block insert if there's either no URL for either of the items, or they don't match + if (bundleUrl == null || !bundleUrl.equals(mapUrl)) { + return true; + } + + var bundleMaps = bundle.get(DataComponentTypes.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.iterate()) { + var data = getImageData(map); + 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 25969d0..d802e15 100644 --- a/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java +++ b/src/main/java/space/essem/image2map/mixin/BundleItemMixin.java @@ -1,10 +1,9 @@ package space.essem.image2map.mixin; -import net.minecraft.component.DataComponentTypes; -import net.minecraft.util.ActionResult; 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 net.minecraft.entity.player.PlayerEntity; @@ -13,44 +12,118 @@ import net.minecraft.item.ItemStack; import net.minecraft.screen.slot.Slot; import net.minecraft.util.ClickType; -import net.minecraft.util.Hand; import net.minecraft.world.World; +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 = "dropContentsOnUse", at = @At("RETURN")) + private void image2map$dropContentsOnUse(World world, PlayerEntity player, ItemStack stack, CallbackInfo ci) { + if (player.isCreative()) { + return; + } - @Inject(method = "use", at = @At("HEAD"), cancellable = true) - private void image2map$useBundle(World world, PlayerEntity user, Hand hand, - CallbackInfoReturnable cir) { - ItemStack itemStack = user.getStackInHand(hand); - var tag = itemStack.get(DataComponentTypes.CUSTOM_DATA); + 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 = "onStackClicked", at = @At("HEAD"), cancellable = true) + private void image2map$onStackClickedHead(ItemStack bundle, Slot slot, ClickType clickType, PlayerEntity player, CallbackInfoReturnable cir) { + if (player.isCreative()) { + return; + } + + if (clickType != ClickType.LEFT) { + return; + } - if (tag != null && tag.contains("image2map:quick_place") && !user.isCreative()) { - cir.setReturnValue(ActionResult.FAIL); - cir.cancel(); + if (Image2Map.isInvalidMapForBundle(bundle, slot.getStack())) { + cir.setReturnValue(false); + } } - } - @Inject(method = "onStackClicked", at = @At("HEAD"), cancellable = true) - private void image2map$addBundleItems(ItemStack bundle, Slot slot, ClickType clickType, PlayerEntity player, - CallbackInfoReturnable cir) { - var tag = bundle.get(DataComponentTypes.CUSTOM_DATA); + /** + * 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 = "onStackClicked", at = @At("RETURN")) + private void image2map$onStackClickedReturn(ItemStack bundle, Slot slot, ClickType clickType, PlayerEntity player, CallbackInfoReturnable cir) { + if (player.isCreative()) { + return; + } - if (tag != null && tag.contains("image2map:quick_place") && !player.isCreative()) { - cir.setReturnValue(false); - cir.cancel(); + if (clickType != ClickType.RIGHT) { + return; + } + + Image2Map.destroyBundleOnEmpty(bundle); } - } - @Inject(method = "onClicked", at = @At("HEAD"), cancellable = true) - private void image2map$removeBundleItems(ItemStack bundle, ItemStack otherStack, Slot slot, ClickType clickType, - PlayerEntity player, StackReference cursorStackReference, - CallbackInfoReturnable cir) { - var tag = bundle.get(DataComponentTypes.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 = "onClicked", at = @At("HEAD"), cancellable = true) + private void image2map$onClickedHead( + ItemStack bundle, + ItemStack otherStack, + Slot slot, + ClickType clickType, + PlayerEntity player, + StackReference cursorStackReference, + CallbackInfoReturnable cir + ) { + if (player.isCreative()) { + return; + } + + if (clickType != ClickType.LEFT) { + return; + } + + if (Image2Map.isInvalidMapForBundle(bundle, otherStack)) { + cir.setReturnValue(false); + } + } + + /** + * 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 = "onClicked", at = @At("RETURN")) + private void image2map$onClickedReturn( + ItemStack bundle, + ItemStack otherStack, + Slot slot, + ClickType clickType, + PlayerEntity player, + StackReference cursorStackReference, + CallbackInfoReturnable cir + ) { + if (player.isCreative()) { + return; + } + + if (clickType != ClickType.RIGHT) { + return; + } - if (tag != null && tag.contains("image2map:quick_place") && !player.isCreative()) { - cir.setReturnValue(false); - cir.cancel(); + Image2Map.destroyBundleOnEmpty(bundle); } - } } From 8ca4e9d26dcac277ee3a496ef64738eb721ec353 Mon Sep 17 00:00:00 2001 From: Leonard Joensen Date: Mon, 28 Jul 2025 14:52:23 +0200 Subject: [PATCH 4/4] Account for create-folder when getting input from lore --- .../java/space/essem/image2map/Image2Map.java | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/main/java/space/essem/image2map/Image2Map.java b/src/main/java/space/essem/image2map/Image2Map.java index 164f13f..c9c0f63 100644 --- a/src/main/java/space/essem/image2map/Image2Map.java +++ b/src/main/java/space/essem/image2map/Image2Map.java @@ -473,7 +473,7 @@ public static boolean clickItemFrame(PlayerEntity player, Hand hand, ItemFrameEn return codec.isSuccess() ? codec.getOrThrow() : null; } - private static @Nullable String getUrlFromLore(ItemStack stack) { + private static @Nullable String getInputPathFromLore(ItemStack stack) { if (getImageData(stack) == null) { return null; } @@ -483,17 +483,7 @@ public static boolean clickItemFrame(PlayerEntity player, Hand hand, ItemFrameEn return null; } - // This could be simplified to lore.lines().getLast().getString() currently, - // however doing it this way ensures future compatibility with any - // added lore elements. - for (var line : lore.lines()) { - var lineString = line.getString(); - if (lineString.startsWith("http://") || lineString.startsWith("https://")) { - return lineString; - } - } - - return null; + return lore.lines().getLast().getString(); } public static boolean destroyItemFrame(ServerWorld serverWorld, Entity player, ItemFrameEntity itemFrameEntity) { @@ -558,9 +548,9 @@ public static boolean destroyItemFrame(ServerWorld serverWorld, Entity player, I } if (!frameItems.isEmpty()) { - String url = getUrlFromLore(frameItems.getFirst()); - if (url == null) { - url = "unknown"; + String inputPath = getInputPathFromLore(frameItems.getFirst()); + if (inputPath == null) { + inputPath = "unknown"; } // Clear the right/down/facing data from the items, @@ -586,7 +576,7 @@ public static boolean destroyItemFrame(ServerWorld serverWorld, Entity player, I ); } - itemFrameEntity.dropStack(serverWorld, toSingleStack(frameItems, url, width * 128, height * 128)); + itemFrameEntity.dropStack(serverWorld, toSingleStack(frameItems, inputPath, width * 128, height * 128)); } return true; @@ -630,11 +620,11 @@ public static boolean isInvalidMapForBundle(ItemStack bundle, ItemStack item) { return true; } - var bundleUrl = getUrlFromLore(bundle); - var mapUrl = getUrlFromLore(item); + var bundleInputPath = getInputPathFromLore(bundle); + var mapInputPath = getInputPathFromLore(item); - // Block insert if there's either no URL for either of the items, or they don't match - if (bundleUrl == null || !bundleUrl.equals(mapUrl)) { + // 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; }