Add compat for /dank/null

This commit is contained in:
trunksbomb
2026-03-22 18:16:45 -04:00
parent fc5b65a53b
commit 3e4fa38528
7 changed files with 425 additions and 16 deletions

View File

@@ -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.

View File

@@ -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));
}
}

View File

@@ -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<BagHandler> 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);
}
}
}

View File

@@ -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) {
}

View File

@@ -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<Void> enqueueWork(Runnable task) {
task.run();
return CompletableFuture.completedFuture(null);
}
@Override
public <T> CompletableFuture<T> enqueueWork(Supplier<T> 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();
}
}

View File

@@ -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<RenderedTab> tabs = getRenderedTabs(inventoryScreen, player);
List<RenderedTab> 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<RenderedTab> getRenderedTabs(AbstractContainerScreen<?> screen, Player player) {
List<BagEntry> bags = BagAccess.findBags(player);
List<RenderedTab> 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<Slot> getPlayerInventorySlots(AbstractContainerScreen<?> screen, Player player) {
List<Slot> 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;
}

View File

@@ -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);
}
}
}