Prepare for second implementation pass - give bags UUIDs, utilize the UUID system of other compatible bag mods (like /dank/null) and support pseudo-unique bags of other mods by name + mod convention. Add a Bag Renamer to freely rename any bag based item.

This commit is contained in:
trunksbomb
2026-03-22 19:36:36 -04:00
parent 52a303784d
commit f40b38ab3c
17 changed files with 474 additions and 2 deletions

View File

@@ -1,7 +1,9 @@
package com.trunksbomb.bagtabs;
import com.mojang.logging.LogUtils;
import com.trunksbomb.bagtabs.item.BagNamerItem;
import com.trunksbomb.bagtabs.item.BagItem;
import com.trunksbomb.bagtabs.menu.BagNamerMenu;
import com.trunksbomb.bagtabs.menu.BagMenu;
import com.trunksbomb.bagtabs.network.BagTabsNetwork;
import net.minecraft.core.registries.Registries;
@@ -38,13 +40,17 @@ public class BagTabs {
.component(DataComponents.CONTAINER, ItemContainerContents.EMPTY)
.component(DataComponents.DYED_COLOR, new DyedItemColor(BagItem.DEFAULT_COLOR, true))
));
public static final DeferredItem<Item> BAG_NAMER = ITEMS.register("bag_namer", () -> new BagNamerItem(new Item.Properties().stacksTo(1)));
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> BAG_TAB = CREATIVE_MODE_TABS.register(
"main",
() -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup." + MODID))
.withTabsBefore(CreativeModeTabs.TOOLS_AND_UTILITIES)
.icon(() -> BAG.get().getDefaultInstance())
.displayItems((parameters, output) -> BagItem.createCreativeStacks().forEach(output::accept))
.displayItems((parameters, output) -> {
BagItem.createCreativeStacks().forEach(output::accept);
output.accept(BAG_NAMER.get());
})
.build()
);
@@ -52,6 +58,10 @@ public class BagTabs {
"bag",
() -> IMenuTypeExtension.create(BagMenu::fromNetwork)
);
public static final DeferredHolder<MenuType<?>, MenuType<BagNamerMenu>> BAG_NAMER_MENU = MENUS.register(
"bag_namer",
() -> IMenuTypeExtension.create(BagNamerMenu::fromNetwork)
);
public BagTabs(IEventBus modEventBus) {
ITEMS.register(modEventBus);

View File

@@ -1,6 +1,7 @@
package com.trunksbomb.bagtabs;
import com.trunksbomb.bagtabs.client.BagScreen;
import com.trunksbomb.bagtabs.client.BagNamerScreen;
import com.trunksbomb.bagtabs.client.BagTabOverlay;
import net.neoforged.bus.api.IEventBus;
import net.minecraft.client.color.item.ItemColor;
@@ -25,6 +26,7 @@ public class BagTabsClient {
private static void registerScreens(RegisterMenuScreensEvent event) {
event.register(BagTabs.BAG_MENU.get(), BagScreen::new);
event.register(BagTabs.BAG_NAMER_MENU.get(), BagNamerScreen::new);
}
private static void registerItemColors(RegisterColorHandlersEvent.Item event) {

View File

@@ -1,5 +1,6 @@
package com.trunksbomb.bagtabs.bag;
import com.trunksbomb.bagtabs.item.BagItem;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.world.entity.player.Inventory;
@@ -16,6 +17,9 @@ public final class BagAccess {
for (int slot = 0; slot < inventory.getContainerSize(); slot++) {
ItemStack stack = inventory.getItem(slot);
if (stack.getItem() instanceof BagItem) {
BagIdentityData.ensureBagId(stack);
}
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
if (handler != null) {
bags.add(new BagEntry(slot, stack.copy(), handler));

View File

@@ -5,12 +5,15 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Locale;
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;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
public final class BagCompat {
private static final List<BagHandler> HANDLERS = List.of(
@@ -41,6 +44,30 @@ public final class BagCompat {
return getActiveBagSlot(menu) >= 0;
}
public static boolean canName(ItemStack stack) {
return !stack.isEmpty() && (findHandler(stack) != null || looksLikeInventoryItem(stack));
}
public static BagIdentity getIdentity(ItemStack stack) {
if (!canName(stack)) {
return null;
}
if (stack.getItem() instanceof InventoryBag) {
return new BagIdentity("bagtabs:uuid:" + BagIdentityData.ensureBagId(stack), "uuid", true);
}
Integer dankFrequency = getDankFrequency(stack);
if (dankFrequency != null && dankFrequency >= 0) {
return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true);
}
String namespace = BuiltInRegistries.ITEM.getKey(stack.getItem()).getNamespace();
String displayName = stack.getHoverName().getString().trim().toLowerCase(Locale.ROOT);
String normalizedName = displayName.replaceAll("\\s+", " ");
return new BagIdentity(namespace + ":" + normalizedName, "name_mod", false);
}
public static int getActiveBagSlot(AbstractContainerMenu menu) {
if (menu instanceof BagMenu bagMenu) {
return bagMenu.getBagSlot();
@@ -114,6 +141,19 @@ public final class BagCompat {
}
}
private static Integer getDankFrequency(ItemStack stack) {
if (!isInstance(DankStorageHandler.ITEM_CLASS, stack.getItem())) {
return null;
}
try {
Object frequency = stack.getItem().getClass().getMethod("getFrequency", ItemStack.class).invoke(null, stack);
return frequency instanceof Integer integer ? integer : null;
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) {
return null;
}
}
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";
@@ -442,6 +482,30 @@ public final class BagCompat {
}
}
private static boolean looksLikeInventoryItem(ItemStack stack) {
if (stack.has(DataComponents.CONTAINER)) {
return true;
}
String itemPath = BuiltInRegistries.ITEM.getKey(stack.getItem()).getPath();
String className = stack.getItem().getClass().getName().toLowerCase(Locale.ROOT);
String lowerPath = itemPath.toLowerCase(Locale.ROOT);
boolean nameHint = lowerPath.contains("bag")
|| lowerPath.contains("backpack")
|| lowerPath.contains("pouch")
|| lowerPath.contains("satchel")
|| lowerPath.contains("dank")
|| lowerPath.contains("shulker")
|| className.contains("bag")
|| className.contains("backpack")
|| className.contains("pouch")
|| className.contains("satchel")
|| className.contains("dank")
|| className.contains("shulker");
return nameHint && stack.getMaxStackSize() == 1;
}
private static Object invoke(Object target, String methodName) {
try {
Method method = target.getClass().getMethod(methodName);

View File

@@ -0,0 +1,4 @@
package com.trunksbomb.bagtabs.bag;
public record BagIdentity(String key, String strategy, boolean stable) {
}

View File

@@ -0,0 +1,40 @@
package com.trunksbomb.bagtabs.bag;
import java.util.UUID;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
public final class BagIdentityData {
private static final String ROOT_KEY = "bagtabs";
private static final String BAG_ID_KEY = "bag_id";
private BagIdentityData() {
}
public static UUID getBagId(ItemStack stack) {
CustomData customData = stack.get(DataComponents.CUSTOM_DATA);
if (customData == null || !customData.contains(ROOT_KEY)) {
return null;
}
CompoundTag root = customData.copyTag().getCompound(ROOT_KEY);
return root.hasUUID(BAG_ID_KEY) ? root.getUUID(BAG_ID_KEY) : null;
}
public static UUID ensureBagId(ItemStack stack) {
UUID existing = getBagId(stack);
if (existing != null) {
return existing;
}
UUID created = UUID.randomUUID();
CustomData.update(DataComponents.CUSTOM_DATA, stack, tag -> {
CompoundTag root = tag.contains(ROOT_KEY, 10) ? tag.getCompound(ROOT_KEY) : new CompoundTag();
root.putUUID(BAG_ID_KEY, created);
tag.put(ROOT_KEY, root);
});
return created;
}
}

View File

@@ -0,0 +1,100 @@
package com.trunksbomb.bagtabs.client;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.menu.BagNamerMenu;
import com.trunksbomb.bagtabs.network.RenameBagPayload;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.neoforged.neoforge.network.PacketDistributor;
public class BagNamerScreen extends AbstractContainerScreen<BagNamerMenu> {
private static final ResourceLocation CONTAINER_BACKGROUND = BagTabs.id("textures/gui/bag_namer.png");
private static final int TEXTURE_WIDTH = 176;
private static final int TEXTURE_HEIGHT = 165;
private EditBox nameBox;
private Button renameButton;
public BagNamerScreen(BagNamerMenu menu, Inventory playerInventory, Component title) {
super(menu, playerInventory, title);
this.imageHeight = 165;
this.inventoryLabelY = this.imageHeight - 94;
}
@Override
protected void init() {
super.init();
this.nameBox = new EditBox(this.font, this.leftPos + 21, this.topPos + 15, 134, 16, BagTabs.translation("gui.bag_namer.name"));
this.nameBox.setMaxLength(48);
this.nameBox.setCanLoseFocus(true);
this.nameBox.setHint(BagTabs.translation("gui.bag_namer.placeholder"));
this.addRenderableWidget(this.nameBox);
this.renameButton = this.addRenderableWidget(Button.builder(BagTabs.translation("gui.bag_namer.rename"), button -> rename())
.bounds(this.leftPos + 64, this.topPos + 44, 48, 20)
.build());
}
@Override
public void containerTick() {
super.containerTick();
this.renameButton.active = this.menu.getSlot(0).hasItem() && !this.menu.getSlot(1).hasItem();
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (this.nameBox.keyPressed(keyCode, scanCode, modifiers) || this.nameBox.canConsumeInput()) {
return true;
}
if (keyCode == 257 || keyCode == 335) {
rename();
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean charTyped(char codePoint, int modifiers) {
return this.nameBox.charTyped(codePoint, modifiers) || super.charTyped(codePoint, modifiers);
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (this.nameBox.mouseClicked(mouseX, mouseY, button)) {
this.setFocused(this.nameBox);
return true;
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
super.render(guiGraphics, mouseX, mouseY, partialTick);
this.renderTooltip(guiGraphics, mouseX, mouseY);
}
@Override
protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) {
guiGraphics.drawString(this.font, this.playerInventoryTitle, this.inventoryLabelX, this.inventoryLabelY, 0x404040, false);
guiGraphics.drawString(this.font, BagTabs.translation("gui.bag_namer.name"), 21, 5, 0x404040, false);
}
@Override
protected void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) {
int left = this.leftPos;
int top = this.topPos;
guiGraphics.blit(CONTAINER_BACKGROUND, left, top, 0, 0, this.imageWidth, this.imageHeight, TEXTURE_WIDTH, TEXTURE_HEIGHT);
}
private void rename() {
PacketDistributor.sendToServer(new RenameBagPayload(this.nameBox.getValue()));
}
}

View File

@@ -2,6 +2,7 @@ package com.trunksbomb.bagtabs.item;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagContainer;
import com.trunksbomb.bagtabs.bag.BagIdentityData;
import com.trunksbomb.bagtabs.bag.InventoryBag;
import com.trunksbomb.bagtabs.menu.BagMenu;
import java.util.ArrayList;
@@ -43,12 +44,14 @@ public class BagItem extends Item implements InventoryBag {
public static ItemStack createColoredStack(int color) {
ItemStack stack = BagTabs.BAG.get().getDefaultInstance();
stack.set(DataComponents.DYED_COLOR, new DyedItemColor(color, true));
BagIdentityData.ensureBagId(stack);
return stack;
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand usedHand) {
ItemStack stack = player.getItemInHand(usedHand);
BagIdentityData.ensureBagId(stack);
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer) {
this.openFromInventory(serverPlayer, usedHand == InteractionHand.MAIN_HAND ? serverPlayer.getInventory().selected : Inventory.SLOT_OFFHAND);
}
@@ -63,6 +66,8 @@ public class BagItem extends Item implements InventoryBag {
return;
}
BagIdentityData.ensureBagId(stack);
Component title = stack.getHoverName();
OptionalInt windowId = player.openMenu(
new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu(

View File

@@ -0,0 +1,33 @@
package com.trunksbomb.bagtabs.item;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.menu.BagNamerMenu;
import java.util.OptionalInt;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
public class BagNamerItem extends Item {
public BagNamerItem(Properties properties) {
super(properties);
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand usedHand) {
ItemStack stack = player.getItemInHand(usedHand);
if (!level.isClientSide && player instanceof ServerPlayer serverPlayer) {
OptionalInt ignored = serverPlayer.openMenu(new SimpleMenuProvider(
(containerId, inventory, ignoredPlayer) -> new BagNamerMenu(containerId, inventory),
Component.translatable("container." + BagTabs.MODID + ".bag_namer")
));
}
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
}
}

View File

@@ -0,0 +1,136 @@
package com.trunksbomb.bagtabs.menu;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagCompat;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
public class BagNamerMenu extends AbstractContainerMenu {
private static final int INPUT_SLOT = 0;
private static final int OUTPUT_SLOT = 1;
private static final int PLAYER_INV_START = 2;
private static final int PLAYER_INV_END = 29;
private static final int HOTBAR_START = 29;
private static final int HOTBAR_END = 38;
private final SimpleContainer renameContainer;
public static BagNamerMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) {
return new BagNamerMenu(containerId, playerInventory);
}
public BagNamerMenu(int containerId, Inventory playerInventory) {
this(containerId, playerInventory, new SimpleContainer(2));
}
private BagNamerMenu(int containerId, Inventory playerInventory, SimpleContainer renameContainer) {
super(BagTabs.BAG_NAMER_MENU.get(), containerId);
checkContainerSize(renameContainer, 2);
this.renameContainer = renameContainer;
renameContainer.startOpen(playerInventory.player);
this.addSlot(new Slot(renameContainer, INPUT_SLOT, 44, 46) {
@Override
public boolean mayPlace(ItemStack stack) {
return BagCompat.canName(stack);
}
});
this.addSlot(new Slot(renameContainer, OUTPUT_SLOT, 116, 46) {
@Override
public boolean mayPlace(ItemStack stack) {
return false;
}
});
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 9; column++) {
int slotIndex = column + row * 9 + 9;
this.addSlot(new Slot(playerInventory, slotIndex, 8 + column * 18, 83 + row * 18));
}
}
for (int column = 0; column < 9; column++) {
this.addSlot(new Slot(playerInventory, column, 8 + column * 18, 141));
}
}
public boolean renameInput(String requestedName) {
ItemStack input = this.renameContainer.getItem(INPUT_SLOT);
ItemStack output = this.renameContainer.getItem(OUTPUT_SLOT);
if (input.isEmpty() || !output.isEmpty() || !BagCompat.canName(input)) {
return false;
}
ItemStack renamed = input.copy();
String name = requestedName.trim();
if (name.isEmpty()) {
renamed.remove(DataComponents.CUSTOM_NAME);
} else {
renamed.set(DataComponents.CUSTOM_NAME, Component.literal(name));
}
this.renameContainer.setItem(OUTPUT_SLOT, renamed);
this.renameContainer.setItem(INPUT_SLOT, ItemStack.EMPTY);
this.broadcastChanges();
return true;
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
Slot slot = this.slots.get(index);
if (slot == null || !slot.hasItem()) {
return ItemStack.EMPTY;
}
ItemStack stack = slot.getItem();
ItemStack copied = stack.copy();
if (index == OUTPUT_SLOT) {
if (!this.moveItemStackTo(stack, PLAYER_INV_START, this.slots.size(), true)) {
return ItemStack.EMPTY;
}
slot.onQuickCraft(stack, copied);
} else if (index == INPUT_SLOT) {
if (!this.moveItemStackTo(stack, PLAYER_INV_START, this.slots.size(), true)) {
return ItemStack.EMPTY;
}
} else if (BagCompat.canName(stack)) {
if (!this.moveItemStackTo(stack, INPUT_SLOT, INPUT_SLOT + 1, false)) {
return ItemStack.EMPTY;
}
} else if (index < HOTBAR_START) {
if (!this.moveItemStackTo(stack, HOTBAR_START, HOTBAR_END, false)) {
return ItemStack.EMPTY;
}
} else if (!this.moveItemStackTo(stack, PLAYER_INV_START, HOTBAR_START, false)) {
return ItemStack.EMPTY;
}
if (stack.isEmpty()) {
slot.setByPlayer(ItemStack.EMPTY);
} else {
slot.setChanged();
}
return copied;
}
@Override
public void removed(Player player) {
super.removed(player);
this.clearContainer(player, this.renameContainer);
this.renameContainer.stopOpen(player);
}
}

View File

@@ -10,6 +10,7 @@ public final class BagTabsNetwork {
registrar.playToServer(OpenBagPayload.TYPE, OpenBagPayload.STREAM_CODEC, OpenBagPayload::handle);
registrar.playToServer(InsertIntoBagPayload.TYPE, InsertIntoBagPayload.STREAM_CODEC, InsertIntoBagPayload::handle);
registrar.playToServer(QueryInsertTargetsPayload.TYPE, QueryInsertTargetsPayload.STREAM_CODEC, QueryInsertTargetsPayload::handle);
registrar.playToServer(RenameBagPayload.TYPE, RenameBagPayload.STREAM_CODEC, RenameBagPayload::handle);
registrar.playToClient(InsertTargetsPayload.TYPE, InsertTargetsPayload.STREAM_CODEC, InsertTargetsPayload::handle);
}
}

View File

@@ -0,0 +1,34 @@
package com.trunksbomb.bagtabs.network;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.menu.BagNamerMenu;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.network.handling.IPayloadContext;
public record RenameBagPayload(String name) implements CustomPacketPayload {
public static final Type<RenameBagPayload> TYPE = new Type<>(BagTabs.id("rename_bag"));
public static final StreamCodec<RegistryFriendlyByteBuf, RenameBagPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.stringUtf8(48),
RenameBagPayload::name,
RenameBagPayload::new
);
@Override
public Type<RenameBagPayload> type() {
return TYPE;
}
public static void handle(RenameBagPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer serverPlayer)) {
return;
}
if (serverPlayer.containerMenu instanceof BagNamerMenu bagNamerMenu) {
bagNamerMenu.renameInput(payload.name());
}
}
}

View File

@@ -1,6 +1,11 @@
{
"item.bagtabs.bag": "Traveler's Bag",
"item.bagtabs.bag_namer": "Bag Namer",
"itemGroup.bagtabs": "Bag Tabs",
"container.bagtabs.bag": "Bag",
"bagtabs.tooltip.click_to_open": "Open from your inventory tabs"
"container.bagtabs.bag_namer": "Bag Namer",
"bagtabs.tooltip.click_to_open": "Open from your inventory tabs",
"bagtabs.gui.bag_namer.name": "New Name",
"bagtabs.gui.bag_namer.placeholder": "Leave blank to clear",
"bagtabs.gui.bag_namer.rename": "Rename"
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "bagtabs:item/bag_namer"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 790 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

View File

@@ -0,0 +1,28 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
" PI",
" NB",
" G"
],
"key": {
"P": {
"item": "minecraft:paper"
},
"I": {
"item": "minecraft:ink_sac"
},
"N": {
"item": "minecraft:name_tag"
},
"B": {
"item": "bagtabs:bag"
},
"G": {
"item": "minecraft:gold_nugget"
}
},
"result": {
"id": "bagtabs:bag_namer"
}
}