From 3e4fa38528b0999f8f605efd403386a6f84e9a78 Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Sun, 22 Mar 2026 18:16:45 -0400 Subject: [PATCH] Add compat for /dank/null --- build.gradle | 4 + .../com/trunksbomb/bagtabs/bag/BagAccess.java | 5 +- .../com/trunksbomb/bagtabs/bag/BagCompat.java | 279 ++++++++++++++++++ .../com/trunksbomb/bagtabs/bag/BagEntry.java | 2 +- .../bagtabs/bag/ServerPayloadContext.java | 84 ++++++ .../bagtabs/client/BagTabOverlay.java | 61 +++- .../bagtabs/network/OpenBagPayload.java | 6 +- 7 files changed, 425 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java create mode 100644 src/main/java/com/trunksbomb/bagtabs/bag/ServerPayloadContext.java diff --git a/build.gradle b/build.gradle index c79a323..adfa4c0 100644 --- a/build.gradle +++ b/build.gradle @@ -127,6 +127,10 @@ dependencies { compileOnly "curse.maven:jei-238222:7391682" localRuntime "curse.maven:jade-324717:6155158" localRuntime "curse.maven:jei-238222:7391682" + localRuntime "curse.maven:travelers-backpack-321117:7485008" + localRuntime "curse.maven:sophisticated-core-618298:7635107" + localRuntime "curse.maven:sophisticated-backpacks-422301:7645643" + localRuntime "curse.maven:dank-storage-335673:7539725" } // This block of code expands all declared replace properties in the specified resource targets. diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java index d17f751..ed1ba46 100644 --- a/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java @@ -16,8 +16,9 @@ public final class BagAccess { for (int slot = 0; slot < inventory.getContainerSize(); slot++) { ItemStack stack = inventory.getItem(slot); - if (stack.getItem() instanceof InventoryBag inventoryBag) { - bags.add(new BagEntry(slot, stack.copy(), inventoryBag)); + BagCompat.BagHandler handler = BagCompat.findHandler(stack); + if (handler != null) { + bags.add(new BagEntry(slot, stack.copy(), handler)); } } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java new file mode 100644 index 0000000..29d7bc3 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java @@ -0,0 +1,279 @@ +package com.trunksbomb.bagtabs.bag; + +import com.trunksbomb.bagtabs.menu.BagMenu; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public final class BagCompat { + private static final List HANDLERS = List.of( + new NativeBagHandler(), + new DankStorageHandler(), + new TravelersBackpackHandler(), + new SophisticatedBackpacksHandler() + ); + + private BagCompat() { + } + + public static BagHandler findHandler(ItemStack stack) { + if (stack.isEmpty()) { + return null; + } + + for (BagHandler handler : HANDLERS) { + if (handler.supports(stack)) { + return handler; + } + } + + return null; + } + + public static boolean supportsMenu(AbstractContainerMenu menu) { + return getActiveBagSlot(menu) >= 0; + } + + public static int getActiveBagSlot(AbstractContainerMenu menu) { + if (menu instanceof BagMenu bagMenu) { + return bagMenu.getBagSlot(); + } + + for (BagHandler handler : HANDLERS) { + int slot = handler.getActiveBagSlot(menu); + if (slot >= 0) { + return slot; + } + } + + return -1; + } + + public interface BagHandler { + boolean supports(ItemStack stack); + + void open(ServerPlayer player, int slot, ItemStack stack); + + default int getActiveBagSlot(AbstractContainerMenu menu) { + return -1; + } + } + + private static final class NativeBagHandler implements BagHandler { + @Override + public boolean supports(ItemStack stack) { + return stack.getItem() instanceof InventoryBag; + } + + @Override + public void open(ServerPlayer player, int slot, ItemStack stack) { + if (stack.getItem() instanceof InventoryBag inventoryBag) { + inventoryBag.openFromInventory(player, slot); + } + } + } + + private static final class TravelersBackpackHandler implements BagHandler { + private static final String ITEM_CLASS = "com.tiviacz.travelersbackpack.items.TravelersBackpackItem"; + private static final String MENU_CLASS = "com.tiviacz.travelersbackpack.inventory.menu.AbstractBackpackMenu"; + private static final String WRAPPER_CLASS = "com.tiviacz.travelersbackpack.inventory.BackpackWrapper"; + private static final String CONTAINER_CLASS = "com.tiviacz.travelersbackpack.inventory.BackpackContainer"; + + @Override + public boolean supports(ItemStack stack) { + return isInstance(ITEM_CLASS, stack.getItem()); + } + + @Override + public void open(ServerPlayer player, int slot, ItemStack stack) { + invokeStatic( + CONTAINER_CLASS, + "openBackpack", + new Class[] {ServerPlayer.class, ItemStack.class, int.class, int.class}, + player, + stack, + 1, + slot + ); + } + + @Override + public int getActiveBagSlot(AbstractContainerMenu menu) { + if (!isInstance(MENU_CLASS, menu)) { + return -1; + } + + Object wrapper = invoke(menu, "getWrapper"); + if (wrapper == null || !isInstance(WRAPPER_CLASS, wrapper)) { + return -1; + } + + Object slot = invoke(wrapper, "getBackpackSlotIndex"); + return slot instanceof Integer integer ? integer : -1; + } + } + + private static final class DankStorageHandler implements BagHandler { + private static final String ITEM_CLASS = "tfar.dankstorage.item.DankItem"; + private static final String MENU_CLASS = "tfar.dankstorage.menu.DankMenu"; + + @Override + public boolean supports(ItemStack stack) { + return isInstance(ITEM_CLASS, stack.getItem()); + } + + @Override + public void open(ServerPlayer player, int slot, ItemStack stack) { + try { + Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class); + Object provider = createProvider.invoke(stack.getItem(), stack); + if (provider instanceof MenuProvider menuProvider) { + player.openMenu(menuProvider); + return; + } + + throw new IllegalStateException("Dank Storage did not return a MenuProvider"); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) { + throw new RuntimeException("Failed to open Dank Storage via compatibility bridge", exception); + } + } + + @Override + public int getActiveBagSlot(AbstractContainerMenu menu) { + if (!isInstance(MENU_CLASS, menu)) { + return -1; + } + + Object bag = invoke(menu, "getBag"); + if (!(bag instanceof ItemStack bagStack) || bagStack.isEmpty()) { + return -1; + } + + return findMatchingPlayerSlot(menu, bagStack); + } + } + + private static final class SophisticatedBackpacksHandler implements BagHandler { + private static final String ITEM_CLASS = "net.p3pp3rf1y.sophisticatedbackpacks.backpack.BackpackItem"; + private static final String MENU_CLASS = "net.p3pp3rf1y.sophisticatedbackpacks.common.gui.BackpackContainer"; + private static final String CONTEXT_CLASS = "net.p3pp3rf1y.sophisticatedbackpacks.common.gui.BackpackContext"; + private static final String PAYLOAD_CLASS = "net.p3pp3rf1y.sophisticatedbackpacks.network.BackpackOpenPayload"; + + @Override + public boolean supports(ItemStack stack) { + return isInstance(ITEM_CLASS, stack.getItem()); + } + + @Override + public void open(ServerPlayer player, int slot, ItemStack stack) { + try { + Class payloadClass = Class.forName(PAYLOAD_CLASS); + Method handlePayload = payloadClass.getMethod( + "handlePayload", + payloadClass, + Class.forName("net.neoforged.neoforge.network.handling.IPayloadContext") + ); + Object payload = payloadClass.getConstructor(int.class).newInstance(slot); + handlePayload.invoke(null, payload, new ServerPayloadContext(player)); + } catch (ReflectiveOperationException exception) { + throw new RuntimeException("Failed to open Sophisticated Backpack via compatibility bridge", exception); + } + } + + @Override + public int getActiveBagSlot(AbstractContainerMenu menu) { + if (!isInstance(MENU_CLASS, menu)) { + return -1; + } + + Object context = invoke(menu, "getBackpackContext"); + if (context == null || !isInstance(CONTEXT_CLASS, context)) { + return -1; + } + + Object slot = invoke(context, "getBackpackSlotIndex"); + return slot instanceof Integer integer ? integer : -1; + } + } + + private static int findMatchingPlayerSlot(AbstractContainerMenu menu, ItemStack bagStack) { + Inventory playerInventory = findPlayerInventory(menu); + if (playerInventory == null) { + return -1; + } + + for (Slot slot : menu.slots) { + if (slot.container != playerInventory) { + continue; + } + + ItemStack slotStack = slot.getItem(); + if (slotStack == bagStack) { + return slot.getContainerSlot(); + } + } + + for (Slot slot : menu.slots) { + if (slot.container != playerInventory) { + continue; + } + + ItemStack slotStack = slot.getItem(); + if (ItemStack.isSameItemSameComponents(slotStack, bagStack)) { + return slot.getContainerSlot(); + } + } + + return -1; + } + + private static Inventory findPlayerInventory(AbstractContainerMenu menu) { + for (Slot slot : menu.slots) { + if (slot.container instanceof Inventory inventory) { + return inventory; + } + } + + try { + Field inventoryField = menu.getClass().getField("playerInventory"); + Object inventory = inventoryField.get(menu); + return inventory instanceof Inventory playerInventory ? playerInventory : null; + } catch (IllegalAccessException | NoSuchFieldException exception) { + return null; + } + } + + private static boolean isInstance(String className, Object instance) { + try { + return instance != null && Class.forName(className).isInstance(instance); + } catch (ClassNotFoundException exception) { + return false; + } + } + + private static Object invoke(Object target, String methodName) { + try { + Method method = target.getClass().getMethod(methodName); + return method.invoke(target); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) { + throw new RuntimeException("Failed to invoke " + methodName + " on " + target.getClass().getName(), exception); + } + } + + private static void invokeStatic(String className, String methodName, Class[] parameterTypes, Object... args) { + try { + Class targetClass = Class.forName(className); + Method method = targetClass.getMethod(methodName, parameterTypes); + method.invoke(null, args); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException exception) { + throw new RuntimeException("Failed to invoke compatibility method " + className + "#" + methodName, exception); + } + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java index 198d541..7bda808 100644 --- a/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java @@ -2,5 +2,5 @@ package com.trunksbomb.bagtabs.bag; import net.minecraft.world.item.ItemStack; -public record BagEntry(int slot, ItemStack stack, InventoryBag bag) { +public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler) { } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/ServerPayloadContext.java b/src/main/java/com/trunksbomb/bagtabs/bag/ServerPayloadContext.java new file mode 100644 index 0000000..ff2f065 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/ServerPayloadContext.java @@ -0,0 +1,84 @@ +package com.trunksbomb.bagtabs.bag; + +import io.netty.channel.ChannelHandlerContext; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import net.minecraft.network.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ConfigurationTask; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.common.extensions.ICommonPacketListener; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public class ServerPayloadContext implements IPayloadContext { + private final ServerPlayer player; + + public ServerPayloadContext(ServerPlayer player) { + this.player = player; + } + + @Override + public ICommonPacketListener listener() { + return (ICommonPacketListener)this.player.connection; + } + + @Override + public Player player() { + return this.player; + } + + @Override + public CompletableFuture enqueueWork(Runnable task) { + task.run(); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletableFuture enqueueWork(Supplier task) { + return CompletableFuture.completedFuture(task.get()); + } + + @Override + public PacketFlow flow() { + return PacketFlow.SERVERBOUND; + } + + @Override + public void handle(CustomPacketPayload payload) { + throw new UnsupportedOperationException("Nested payload handling is not supported by this compatibility context"); + } + + @Override + public void finishCurrentTask(ConfigurationTask.Type type) { + throw new UnsupportedOperationException("Configuration tasks are not supported by this compatibility context"); + } + + @Override + public Connection connection() { + return this.player.connection.getConnection(); + } + + @Override + public void reply(CustomPacketPayload payload) { + this.player.connection.send(payload); + } + + @Override + public void disconnect(Component reason) { + this.player.connection.disconnect(reason); + } + + @Override + public void handle(Packet packet) { + throw new UnsupportedOperationException("Packet forwarding is not supported by this compatibility context"); + } + + @Override + public ChannelHandlerContext channelHandlerContext() { + return this.player.connection.getConnection().channel().pipeline().lastContext(); + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index 32e1787..e54e222 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -2,8 +2,10 @@ package com.trunksbomb.bagtabs.client; import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.bag.BagAccess; +import com.trunksbomb.bagtabs.bag.BagCompat; import com.trunksbomb.bagtabs.bag.BagEntry; import com.trunksbomb.bagtabs.item.BagItem; +import com.trunksbomb.bagtabs.menu.BagMenu; import com.trunksbomb.bagtabs.network.OpenBagPayload; import com.mojang.blaze3d.systems.RenderSystem; import java.util.ArrayList; @@ -11,12 +13,12 @@ import java.util.List; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.client.gui.screens.inventory.InventoryScreen; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.component.DyedItemColor; +import net.minecraft.world.inventory.Slot; import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.network.PacketDistributor; @@ -27,13 +29,13 @@ public final class BagTabOverlay { private static final int TAB_HEIGHT = 22; private static final int TAB_GAP = 0; private static final int TAB_Y_OFFSET = -3; - private static final int TAB_X_OFFSET = 7; + private static final int TAB_X_OFFSET = -6; private BagTabOverlay() { } public static void render(ScreenEvent.Render.Post event) { - if (!(event.getScreen() instanceof InventoryScreen inventoryScreen)) { + if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen)) { return; } @@ -42,7 +44,7 @@ public final class BagTabOverlay { return; } - List tabs = getRenderedTabs(inventoryScreen, player); + List tabs = getRenderedTabs(screen, player); if (tabs.isEmpty()) { return; } @@ -65,7 +67,7 @@ public final class BagTabOverlay { } public static void mouseClicked(ScreenEvent.MouseButtonPressed.Pre event) { - if (!(event.getScreen() instanceof InventoryScreen inventoryScreen) || event.getButton() != 0) { + if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen) || event.getButton() != 0) { return; } @@ -74,7 +76,7 @@ public final class BagTabOverlay { return; } - for (RenderedTab tab : getRenderedTabs(inventoryScreen, player)) { + for (RenderedTab tab : getRenderedTabs(screen, player)) { if (tab.isHovered(event.getMouseX(), event.getMouseY())) { PacketDistributor.sendToServer(new OpenBagPayload(tab.entry().slot())); Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); @@ -87,37 +89,74 @@ public final class BagTabOverlay { private static List getRenderedTabs(AbstractContainerScreen screen, Player player) { List bags = BagAccess.findBags(player); List renderedTabs = new ArrayList<>(); - int x = screen.getGuiLeft() + TAB_X_OFFSET; + int activeBagSlot = getActiveBagSlot(screen); + int leftBound = getInventoryLeftBound(screen, player); + int rightBound = getInventoryRightBound(screen, player); + int x = leftBound + TAB_X_OFFSET; int y = screen.getGuiTop() + screen.getYSize() + TAB_Y_OFFSET; - int maxX = screen.getGuiLeft() + screen.getXSize() - TAB_WIDTH; + int maxX = rightBound - TAB_WIDTH; for (BagEntry bag : bags) { if (x > maxX) { break; } - renderedTabs.add(new RenderedTab(bag, x, y)); + renderedTabs.add(new RenderedTab(bag, x, y, bag.slot() == activeBagSlot)); x += TAB_WIDTH + TAB_GAP; } return renderedTabs; } + private static int getInventoryLeftBound(AbstractContainerScreen screen, Player player) { + return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream() + .mapToInt(slot -> slot.x) + .min() + .orElse(8); + } + + private static int getInventoryRightBound(AbstractContainerScreen screen, Player player) { + return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream() + .mapToInt(slot -> slot.x + 16) + .max() + .orElse(screen.getXSize() - 8); + } + + private static List getPlayerInventorySlots(AbstractContainerScreen screen, Player player) { + List slots = screen.getMenu().slots.stream() + .filter(slot -> slot.container == player.getInventory()) + .toList(); + return slots.isEmpty() ? screen.getMenu().slots : slots; + } + private static void renderTab(GuiGraphics guiGraphics, RenderedTab tab, int mouseX, int mouseY) { boolean hovered = tab.isHovered(mouseX, mouseY); + boolean selected = tab.selected(); int color = DyedItemColor.getOrDefault(tab.entry().stack(), BagItem.DEFAULT_COLOR); float red = ((color >> 16) & 0xFF) / 255.0F; float green = ((color >> 8) & 0xFF) / 255.0F; float blue = (color & 0xFF) / 255.0F; - int uOffset = hovered ? TAB_WIDTH : 0; + int uOffset = (hovered || selected) ? TAB_WIDTH : 0; RenderSystem.setShaderColor(red, green, blue, 1.0F); guiGraphics.blit(TAB_BASE_TEXTURE, tab.x(), tab.y(), uOffset, 0, TAB_WIDTH, TAB_HEIGHT, TAB_WIDTH * 2, TAB_HEIGHT); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); guiGraphics.blit(TAB_OVERLAY_TEXTURE, tab.x(), tab.y(), uOffset, 0, TAB_WIDTH, TAB_HEIGHT, TAB_WIDTH * 2, TAB_HEIGHT); + + if (selected) { + guiGraphics.fill(tab.x() + 2, tab.y() + 2, tab.x() + TAB_WIDTH - 2, tab.y() + 4, 0x90FFFFFF); + } } - private record RenderedTab(BagEntry entry, int x, int y) { + private static boolean supportsTabs(AbstractContainerScreen screen) { + return screen instanceof net.minecraft.client.gui.screens.inventory.InventoryScreen || BagCompat.supportsMenu(screen.getMenu()); + } + + private static int getActiveBagSlot(AbstractContainerScreen screen) { + return BagCompat.getActiveBagSlot(screen.getMenu()); + } + + private record RenderedTab(BagEntry entry, int x, int y, boolean selected) { private boolean isHovered(double mouseX, double mouseY) { return mouseX >= this.x && mouseX < this.x + TAB_WIDTH && mouseY >= this.y && mouseY < this.y + TAB_HEIGHT; } diff --git a/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java b/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java index 6920c87..c95b24f 100644 --- a/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java +++ b/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java @@ -1,6 +1,7 @@ package com.trunksbomb.bagtabs.network; import com.trunksbomb.bagtabs.BagTabs; +import com.trunksbomb.bagtabs.bag.BagCompat; import com.trunksbomb.bagtabs.bag.InventoryBag; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; @@ -33,8 +34,9 @@ public record OpenBagPayload(int slot) implements CustomPacketPayload { } ItemStack stack = serverPlayer.getInventory().getItem(payload.slot()); - if (stack.getItem() instanceof InventoryBag inventoryBag) { - inventoryBag.openFromInventory(serverPlayer, payload.slot()); + BagCompat.BagHandler handler = BagCompat.findHandler(stack); + if (handler != null) { + handler.open(serverPlayer, payload.slot(), stack); } } }