diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 5dd08d0..25a60fd 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -7,7 +7,9 @@ on: jobs: publish: - runs-on: ubuntu-latest + # In Gitea, this must match a label advertised by one of your online runners. + # Set it in repository variables, for example: GITEA_RUNNER_LABEL=windows or linux_amd64. + runs-on: ${{ vars.GITEA_RUNNER_LABEL }} env: GITEA_SERVER_URL: ${{ gitea.server_url }} BAGTABS_GITEA_TOKEN: ${{ secrets.BAGTABS_GITEA_TOKEN }} @@ -33,11 +35,23 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - - name: Make Gradle wrapper executable - run: chmod +x ./gradlew - - name: Build release jar - run: ./gradlew --no-daemon clean build + shell: pwsh + run: | + if (Test-Path ./gradlew.bat) { + ./gradlew.bat --no-daemon clean build + } else { + chmod +x ./gradlew + ./gradlew --no-daemon clean build + } - name: Publish release artifacts - run: python3 ./scripts/publish_release.py + shell: pwsh + run: | + if (Get-Command python -ErrorAction SilentlyContinue) { + python ./scripts/publish_release.py + } elseif (Get-Command python3 -ErrorAction SilentlyContinue) { + python3 ./scripts/publish_release.py + } else { + throw "Python is not available on PATH." + } diff --git a/API.md b/API.md new file mode 100644 index 0000000..fb8ce98 --- /dev/null +++ b/API.md @@ -0,0 +1,105 @@ +# Bag Tabs API + +Bag Tabs exposes a small runtime API so other mods can make their portable storage items fully compatible without needing Bag Tabs to ship a built-in compat layer. + +There are two pieces: + +- Core API: register a bag provider +- Optional Curios helper: expose Curios slot tag keys for wearable bags + +## Dependency Setup + +Add Bag Tabs as a compile-time dependency in your development environment. + +Your mod only needs the Bag Tabs jar present at runtime if you want to call the API directly. If you want Bag Tabs compatibility to stay optional, gate registration behind a Bag Tabs loaded check in your own mod. + +## Core API + +Register a provider with: + +`com.trunksbomb.bagtabs.api.BagTabsApi.registerProvider(...)` + +Implement: + +- [BagTabsProvider](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsProvider.java) + +Supporting types: + +- [BagTabsIdentity](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsIdentity.java) +- [BagTabsLocation](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\BagTabsLocation.java) + +### What a provider can do + +- declare whether an `ItemStack` is one of your bags +- provide a stable or semi-stable identity for pinning and ordering +- open the bag natively +- support drag-insert from the carried stack +- report whether a menu is the active/open bag menu +- report fullness for the fullness indicator + +### Minimal Example + +A concrete copy-and-adapt example lives here: + +- [ExampleBagTabsCompat.java](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\test\java\com\trunksbomb\bagtabs\api\examples\ExampleBagTabsCompat.java) + +## Identity Rules + +Use a stable identity whenever possible. + +Recommended: + +- UUID stored on the item stack +- your mod's own saved container id +- another durable unique id already used by your bag system + +Fallback only if you truly do not have a unique identifier: + +- `modid + normalized display name` + +That fallback behaves as semi-unique, which means only one matching bag can be pinned at a time. + +## Optional Curios Helper + +If your bag is wearable in Curios, you do not need a second Bag Tabs provider. + +Register the same core provider once, then mark your item as wearable in Curios with the helper constants/tag keys from: + +- [BagTabsCuriosApi](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\src\main\java\com\trunksbomb\bagtabs\api\curios\BagTabsCuriosApi.java) + +Helpers: + +- `BagTabsCuriosApi.BACK_SLOT` +- `BagTabsCuriosApi.BELT_SLOT` +- `BagTabsCuriosApi.backTag()` +- `BagTabsCuriosApi.beltTag()` +- `BagTabsCuriosApi.tagForSlot(...)` + +### Curios Example + +If your item already has a provider registered with the core API, Bag Tabs will use that same provider when the item is found in a Curios slot. + +You only need to make the item wearable in Curios, for example by adding Curios item tags: + +```json +{ + "replace": false, + "values": [ + "examplemod:example_bag" + ] +} +``` + +For a back slot tag file: + +`data/curios/tags/item/back.json` + +For a belt slot tag file: + +`data/curios/tags/item/belt.json` + +## Notes + +- The Bag Tabs API is runtime registration, not a datapack format. +- One provider is enough for both normal inventory support and Curios-worn support. +- Curios support is optional; the core Bag Tabs API does not require Curios classes. diff --git a/README.md b/README.md index af15c69..45a0711 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The goal is simple: if you are carrying a bag, you should be able to open it dir - Adds a tab bar to supported inventory screens. - Shows one tab for each carried bag or portable storage item the mod recognizes. +- Checks supported Curios-equipped storage items as well as the normal player inventory. - Lets you click a tab to open that bag directly. - Keeps the tab bar visible while you move between supported bag screens. - Highlights the currently open bag. @@ -18,6 +19,7 @@ The goal is simple: if you are carrying a bag, you should be able to open it dir - A built-in Bag Tabs bag item. - 27 inventory slots, like a single chest. - Dyeable bag colors. +- Curios-compatible in the back and belt slots when Curios is installed. - Crafting recipe. - Creative tab with the base bag, all dyed variants, and the Bag Namer tool. - JEI entries for the dyed bag variants. @@ -69,6 +71,7 @@ Bag Tabs currently includes compatibility support for: - Traveler's Backpack - Sophisticated Backpacks - Dank Storage +- Curios-equipped storage items are also detected when Curios is installed Other portable storage items may still appear as tabs and may still open through the fallback hotbar method. @@ -94,3 +97,4 @@ Bag Tabs includes a Bag Namer tool that can rename portable storage items. - Bag Tabs is designed to stay attached to existing inventory screens rather than replacing them. - Unsupported bags may behave differently depending on how their mod handles right-click opening. - Some mods may still need dedicated compatibility if they do not expose normal item-use behavior. +- Mod authors can integrate directly through the Bag Tabs API described in [API.md](C:\Users\JessePersonal\Documents\Codex\minecraft-bag-tabs\API.md). diff --git a/build.gradle b/build.gradle index adfa4c0..9f0ce22 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,9 @@ repositories { includeGroup "curse.maven" } } + maven { + url = uri("https://maven.theillusivec4.top/") + } } base { @@ -124,7 +127,9 @@ configurations { dependencies { // Dev-runtime helpers for the client. + compileOnly "top.theillusivec4.curios:curios-neoforge:9.5.1+1.21.1:api" compileOnly "curse.maven:jei-238222:7391682" + localRuntime "top.theillusivec4.curios:curios-neoforge:9.5.1+1.21.1" localRuntime "curse.maven:jade-324717:6155158" localRuntime "curse.maven:jei-238222:7391682" localRuntime "curse.maven:travelers-backpack-321117:7485008" diff --git a/src/main/java/com/trunksbomb/bagtabs/api/BagTabsApi.java b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsApi.java new file mode 100644 index 0000000..3f83a22 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsApi.java @@ -0,0 +1,22 @@ +package com.trunksbomb.bagtabs.api; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public final class BagTabsApi { + private static final List PROVIDERS = new CopyOnWriteArrayList<>(); + + private BagTabsApi() { + } + + public static void registerProvider(BagTabsProvider provider) { + if (provider == null) { + throw new IllegalArgumentException("provider cannot be null"); + } + PROVIDERS.add(provider); + } + + public static List getProviders() { + return List.copyOf(PROVIDERS); + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/api/BagTabsIdentity.java b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsIdentity.java new file mode 100644 index 0000000..ebf47c4 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsIdentity.java @@ -0,0 +1,4 @@ +package com.trunksbomb.bagtabs.api; + +public record BagTabsIdentity(String key, String strategy, boolean stable) { +} diff --git a/src/main/java/com/trunksbomb/bagtabs/api/BagTabsLocation.java b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsLocation.java new file mode 100644 index 0000000..aad594a --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsLocation.java @@ -0,0 +1,13 @@ +package com.trunksbomb.bagtabs.api; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public interface BagTabsLocation { + ItemStack getStack(Player player); + + boolean setStack(Player player, ItemStack stack); + + boolean moveToHotbar(ServerPlayer player, int hotbarSlot); +} diff --git a/src/main/java/com/trunksbomb/bagtabs/api/BagTabsProvider.java b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsProvider.java new file mode 100644 index 0000000..474c67d --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/api/BagTabsProvider.java @@ -0,0 +1,30 @@ +package com.trunksbomb.bagtabs.api; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; + +public interface BagTabsProvider { + boolean supports(ItemStack stack); + + BagTabsIdentity getIdentity(ItemStack stack); + + void open(ServerPlayer player, int slot, BagTabsLocation location, ItemStack stack); + + default boolean canInsertFromCarried(ServerPlayer player, int slot, BagTabsLocation location, ItemStack bagStack, ItemStack carriedStack) { + return false; + } + + default boolean insertFromCarried(ServerPlayer player, int slot, BagTabsLocation location, ItemStack bagStack, ItemStack carriedStack) { + return false; + } + + default boolean isActiveMenu(AbstractContainerMenu menu, Player player, int slot, BagTabsLocation location, ItemStack stack) { + return false; + } + + default float getFullness(ItemStack stack) { + return -1.0F; + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/api/curios/BagTabsCuriosApi.java b/src/main/java/com/trunksbomb/bagtabs/api/curios/BagTabsCuriosApi.java new file mode 100644 index 0000000..8bdfeb5 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/api/curios/BagTabsCuriosApi.java @@ -0,0 +1,26 @@ +package com.trunksbomb.bagtabs.api.curios; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.ItemTags; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; + +public final class BagTabsCuriosApi { + public static final String BACK_SLOT = "back"; + public static final String BELT_SLOT = "belt"; + + private BagTabsCuriosApi() { + } + + public static TagKey backTag() { + return tagForSlot(BACK_SLOT); + } + + public static TagKey beltTag() { + return tagForSlot(BELT_SLOT); + } + + public static TagKey tagForSlot(String slotIdentifier) { + return ItemTags.create(ResourceLocation.fromNamespaceAndPath("curios", slotIdentifier)); + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java index 5ec7a68..ee0b70f 100644 --- a/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagAccess.java @@ -3,6 +3,9 @@ package com.trunksbomb.bagtabs.bag; import com.trunksbomb.bagtabs.item.BagItem; import java.util.ArrayList; import java.util.List; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -22,10 +25,47 @@ public final class BagAccess { } BagCompat.BagHandler handler = BagCompat.findHandler(stack); if (handler != null || BagCompat.canShowInTabs(stack)) { - bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack))); + bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack), new InventoryBagLocation(slot))); + } + } + + Set usedSyntheticSlots = new HashSet<>(); + for (BagEntry bag : bags) { + usedSyntheticSlots.add(bag.slot()); + } + + for (CuriosCompat.CuriosSlotRef curiosSlot : CuriosCompat.getCuriosSlots(player)) { + ItemStack stack = curiosSlot.stack(); + if (stack.getItem() instanceof BagItem) { + BagIdentityData.ensureBagId(stack); + } + BagCompat.BagHandler handler = BagCompat.findHandler(stack); + if (handler != null || BagCompat.canShowInTabs(stack)) { + int syntheticSlot = inventory.getContainerSize() + + 1024 + + Math.floorMod(Objects.hash(curiosSlot.identifier(), curiosSlot.slotIndex()), 1_000_000_000); + while (!usedSyntheticSlots.add(syntheticSlot)) { + syntheticSlot++; + } + bags.add(new BagEntry( + syntheticSlot, + stack.copy(), + handler, + BagCompat.getIdentity(stack), + new CuriosBagLocation(curiosSlot.identifier(), curiosSlot.slotIndex()) + )); } } return bags; } + + public static BagEntry findBag(Player player, int slot) { + for (BagEntry bag : findBags(player)) { + if (bag.slot() == slot) { + return bag; + } + } + return null; + } } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java index a8d5f08..cf0fdfe 100644 --- a/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagCompat.java @@ -1,5 +1,8 @@ package com.trunksbomb.bagtabs.bag; +import com.trunksbomb.bagtabs.api.BagTabsApi; +import com.trunksbomb.bagtabs.api.BagTabsIdentity; +import com.trunksbomb.bagtabs.api.BagTabsProvider; import com.trunksbomb.bagtabs.menu.BagMenu; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -39,11 +42,17 @@ public final class BagCompat { } } + for (BagTabsProvider provider : BagTabsApi.getProviders()) { + if (provider.supports(stack)) { + return new ApiBagHandler(provider); + } + } + return null; } - public static boolean supportsMenu(AbstractContainerMenu menu) { - return getActiveBagSlot(menu) >= 0; + public static boolean supportsMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player) { + return getActiveBagSlot(menu, player) >= 0; } public static boolean canName(ItemStack stack) { @@ -63,6 +72,14 @@ public final class BagCompat { return new BagIdentity("bagtabs:uuid:" + BagIdentityData.ensureBagId(stack), "uuid", true); } + BagHandler handler = findHandler(stack); + if (handler instanceof ApiBagHandler apiHandler) { + BagTabsIdentity identity = apiHandler.provider().getIdentity(stack); + if (identity != null) { + return new BagIdentity(identity.key(), identity.strategy(), identity.stable()); + } + } + Integer dankFrequency = getDankFrequency(stack); if (dankFrequency != null && dankFrequency >= 0) { return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true); @@ -74,7 +91,7 @@ public final class BagCompat { return new BagIdentity(namespace + ":" + normalizedName, "name_mod", false); } - public static int getActiveBagSlot(AbstractContainerMenu menu) { + public static int getActiveBagSlot(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player) { if (menu instanceof BagMenu bagMenu) { return bagMenu.getBagSlot(); } @@ -86,6 +103,14 @@ public final class BagCompat { } } + if (player != null) { + for (BagEntry bag : BagAccess.findBags(player)) { + if (bag.handler() != null && bag.handler().isActiveMenu(menu, player, bag.slot(), bag.location(), bag.stack())) { + return bag.slot(); + } + } + } + return -1; } @@ -110,13 +135,13 @@ public final class BagCompat { public interface BagHandler { boolean supports(ItemStack stack); - void open(ServerPlayer player, int slot, ItemStack stack); + void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack); - default boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + default boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { return false; } - default boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + default boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { return false; } @@ -124,11 +149,47 @@ public final class BagCompat { return -1; } + default boolean isActiveMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player, int slot, BagLocation location, ItemStack stack) { + return this.getActiveBagSlot(menu) == slot; + } + default float getFullness(ItemStack stack) { return -1.0F; } } + private record ApiBagHandler(BagTabsProvider provider) implements BagHandler { + @Override + public boolean supports(ItemStack stack) { + return this.provider.supports(stack); + } + + @Override + public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) { + this.provider.open(player, slot, location, stack); + } + + @Override + public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { + return this.provider.canInsertFromCarried(player, slot, location, bagStack, carriedStack); + } + + @Override + public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { + return this.provider.insertFromCarried(player, slot, location, bagStack, carriedStack); + } + + @Override + public boolean isActiveMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player, int slot, BagLocation location, ItemStack stack) { + return this.provider.isActiveMenu(menu, player, slot, location, stack); + } + + @Override + public float getFullness(ItemStack stack) { + return this.provider.getFullness(stack); + } + } + private static final class NativeBagHandler implements BagHandler { @Override public boolean supports(ItemStack stack) { @@ -136,14 +197,16 @@ public final class BagCompat { } @Override - public void open(ServerPlayer player, int slot, ItemStack stack) { - if (stack.getItem() instanceof InventoryBag inventoryBag) { + public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) { + if (stack.getItem() instanceof com.trunksbomb.bagtabs.item.BagItem bagItem) { + bagItem.openFromLocation(player, location, slot); + } else if (location instanceof InventoryBagLocation && stack.getItem() instanceof InventoryBag inventoryBag) { inventoryBag.openFromInventory(player, slot); } } @Override - public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) { return false; } @@ -152,11 +215,11 @@ public final class BagCompat { return bagMenu.insertCarriedStack(carriedStack); } - return BagContainer.insertInto(new BagContainer(player, slot), carriedStack); + return BagContainer.insertInto(new BagContainer(player, location), carriedStack); } @Override - public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) { return false; } @@ -165,7 +228,7 @@ public final class BagCompat { return bagMenu.canInsertCarriedStack(carriedStack); } - return BagContainer.canInsertInto(new BagContainer(player, slot), carriedStack); + return BagContainer.canInsertInto(new BagContainer(player, location), carriedStack); } @Override @@ -199,7 +262,7 @@ public final class BagCompat { } @Override - public void open(ServerPlayer player, int slot, ItemStack stack) { + public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) { invokeStatic( CONTAINER_CLASS, "openBackpack", @@ -227,7 +290,7 @@ public final class BagCompat { } @Override - public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty()) { return false; } @@ -237,7 +300,7 @@ public final class BagCompat { } @Override - public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty()) { return false; } @@ -284,7 +347,7 @@ public final class BagCompat { } @Override - public void open(ServerPlayer player, int slot, ItemStack stack) { + public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) { try { Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class); Object provider = createProvider.invoke(stack.getItem(), stack); @@ -314,7 +377,7 @@ public final class BagCompat { } @Override - public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) { return false; } @@ -324,7 +387,7 @@ public final class BagCompat { } @Override - public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) { return false; } @@ -410,7 +473,7 @@ public final class BagCompat { } @Override - public void open(ServerPlayer player, int slot, ItemStack stack) { + public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) { try { Class payloadClass = Class.forName(PAYLOAD_CLASS); Method handlePayload = payloadClass.getMethod( @@ -441,7 +504,7 @@ public final class BagCompat { } @Override - public boolean canInsertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean canInsertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty()) { return false; } @@ -451,7 +514,7 @@ public final class BagCompat { } @Override - public boolean insertFromCarried(ServerPlayer player, int slot, ItemStack bagStack, ItemStack carriedStack) { + public boolean insertFromCarried(ServerPlayer player, int slot, BagLocation location, ItemStack bagStack, ItemStack carriedStack) { if (carriedStack.isEmpty()) { return false; } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagContainer.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagContainer.java index 17df61d..c141f8e 100644 --- a/src/main/java/com/trunksbomb/bagtabs/bag/BagContainer.java +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagContainer.java @@ -10,14 +10,14 @@ import net.minecraft.world.item.component.ItemContainerContents; public class BagContainer extends SimpleContainer { private final Player player; - private final int slot; + private final BagLocation location; - public BagContainer(Player player, int slot) { + public BagContainer(Player player, BagLocation location) { super(BagItem.SLOT_COUNT); this.player = player; - this.slot = slot; + this.location = location; - ItemContainerContents contents = player.getInventory().getItem(slot).getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY); + ItemContainerContents contents = location.getStack(player).getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY); contents.copyInto(this.getItems()); } @@ -29,10 +29,12 @@ public class BagContainer extends SimpleContainer { @Override public void setChanged() { super.setChanged(); - ItemStack bagStack = this.player.getInventory().getItem(this.slot); + ItemStack bagStack = this.location.getStack(this.player).copy(); if (!bagStack.isEmpty() && bagStack.getItem() instanceof BagItem) { bagStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.copyItems())); - this.player.getInventory().setChanged(); + if (this.location.setStack(this.player, bagStack)) { + this.player.getInventory().setChanged(); + } } } @@ -42,7 +44,7 @@ public class BagContainer extends SimpleContainer { return false; } - ItemStack current = player.getInventory().getItem(this.slot); + ItemStack current = this.location.getStack(player); return current.getItem() instanceof BagItem; } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagEntry.java index 217a04a..3e7497c 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, BagCompat.BagHandler handler, BagIdentity identity) { +public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler, BagIdentity identity, BagLocation location) { } diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/BagLocation.java b/src/main/java/com/trunksbomb/bagtabs/bag/BagLocation.java new file mode 100644 index 0000000..1c0407c --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/BagLocation.java @@ -0,0 +1,6 @@ +package com.trunksbomb.bagtabs.bag; + +import com.trunksbomb.bagtabs.api.BagTabsLocation; + +public interface BagLocation extends BagTabsLocation { +} diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/CuriosBagLocation.java b/src/main/java/com/trunksbomb/bagtabs/bag/CuriosBagLocation.java new file mode 100644 index 0000000..7383abb --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/CuriosBagLocation.java @@ -0,0 +1,22 @@ +package com.trunksbomb.bagtabs.bag; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public record CuriosBagLocation(String identifier, int slotIndex) implements BagLocation { + @Override + public ItemStack getStack(Player player) { + return CuriosCompat.getStack(player, this.identifier, this.slotIndex); + } + + @Override + public boolean setStack(Player player, ItemStack stack) { + return CuriosCompat.setStack(player, this.identifier, this.slotIndex, stack); + } + + @Override + public boolean moveToHotbar(ServerPlayer player, int hotbarSlot) { + return CuriosCompat.moveToHotbar(player, this.identifier, this.slotIndex, hotbarSlot); + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/CuriosCompat.java b/src/main/java/com/trunksbomb/bagtabs/bag/CuriosCompat.java new file mode 100644 index 0000000..85ac8e2 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/CuriosCompat.java @@ -0,0 +1,183 @@ +package com.trunksbomb.bagtabs.bag; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public final class CuriosCompat { + private static final String API_CLASS = "top.theillusivec4.curios.api.CuriosApi"; + + private CuriosCompat() { + } + + public static List getCuriosSlots(Player player) { + Object curiosInventory = getCuriosInventory(player); + if (curiosInventory == null) { + return List.of(); + } + + Object curiosMapObject = invoke(curiosInventory, "getCurios"); + if (!(curiosMapObject instanceof Map curiosMap)) { + return List.of(); + } + + List slots = new ArrayList<>(); + curiosMap.entrySet().stream() + .sorted(Comparator.comparing(entry -> String.valueOf(entry.getKey()))) + .forEach(entry -> addSlots(slots, String.valueOf(entry.getKey()), entry.getValue())); + return slots; + } + + public static boolean isAvailable() { + try { + Class.forName(API_CLASS); + return true; + } catch (ClassNotFoundException exception) { + return false; + } + } + + public static ItemStack getStack(Player player, String identifier, int slotIndex) { + Object stacks = getCurioStacks(player, identifier); + if (stacks == null) { + return ItemStack.EMPTY; + } + + Object stack = tryInvoke(stacks, "getStackInSlot", new Class[] {int.class}, slotIndex); + return stack instanceof ItemStack itemStack ? itemStack : ItemStack.EMPTY; + } + + public static boolean setStack(Player player, String identifier, int slotIndex, ItemStack stack) { + Object stacks = getCurioStacks(player, identifier); + if (stacks == null) { + return false; + } + + if (!invokeVoid(stacks, "setStackInSlot", new Class[] {int.class, ItemStack.class}, slotIndex, stack.copy())) { + return false; + } + + Object handler = getCurioHandler(player, identifier); + if (handler != null) { + tryInvoke(handler, "setChanged"); + } + return true; + } + + public static boolean moveToHotbar(ServerPlayer player, String identifier, int slotIndex, int hotbarSlot) { + ItemStack sourceStack = getStack(player, identifier, slotIndex).copy(); + if (sourceStack.isEmpty()) { + return false; + } + + ItemStack targetStack = player.getInventory().getItem(hotbarSlot).copy(); + if (!setStack(player, identifier, slotIndex, targetStack)) { + return false; + } + + player.getInventory().setItem(hotbarSlot, sourceStack); + return true; + } + + private static void addSlots(List slots, String identifier, Object curiosHandler) { + Object stacks = tryInvoke(curiosHandler, "getStacks"); + if (stacks == null) { + return; + } + + Integer slotCount = getSlotCount(curiosHandler, stacks); + if (slotCount == null) { + return; + } + + for (int slotIndex = 0; slotIndex < slotCount; slotIndex++) { + Object stack = tryInvoke(stacks, "getStackInSlot", new Class[] {int.class}, slotIndex); + if (stack instanceof ItemStack itemStack && !itemStack.isEmpty()) { + slots.add(new CuriosSlotRef(identifier, slotIndex, itemStack)); + } + } + } + + private static Integer getSlotCount(Object curiosHandler, Object stacks) { + Object count = tryInvoke(curiosHandler, "getSlots"); + if (!(count instanceof Integer)) { + count = tryInvoke(stacks, "getSlots"); + } + return count instanceof Integer integer ? integer : null; + } + + private static Object getCuriosInventory(Player player) { + try { + Class curiosApi = Class.forName(API_CLASS); + Method method = curiosApi.getMethod("getCuriosInventory", LivingEntity.class); + Object result = method.invoke(null, player); + if (result instanceof Optional optional) { + return optional.orElse(null); + } + return result; + } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) { + return null; + } + } + + private static Object getCurioHandler(Player player, String identifier) { + Object curiosInventory = getCuriosInventory(player); + if (curiosInventory == null) { + return null; + } + + Object curiosMapObject = invoke(curiosInventory, "getCurios"); + if (!(curiosMapObject instanceof Map curiosMap)) { + return null; + } + + return curiosMap.get(identifier); + } + + private static Object getCurioStacks(Player player, String identifier) { + Object handler = getCurioHandler(player, identifier); + return handler == null ? null : tryInvoke(handler, "getStacks"); + } + + private static Object invoke(Object target, String methodName) { + Object result = tryInvoke(target, methodName); + if (result == null) { + throw new RuntimeException("Failed to invoke " + methodName + " on " + target.getClass().getName()); + } + return result; + } + + private static Object tryInvoke(Object target, String methodName, Class[] parameterTypes, Object... args) { + try { + Method method = target.getClass().getMethod(methodName, parameterTypes); + return method.invoke(target, args); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) { + return null; + } + } + + private static Object tryInvoke(Object target, String methodName) { + return tryInvoke(target, methodName, new Class[0]); + } + + private static boolean invokeVoid(Object target, String methodName, Class[] parameterTypes, Object... args) { + try { + Method method = target.getClass().getMethod(methodName, parameterTypes); + method.invoke(target, args); + return true; + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException exception) { + return false; + } + } + + public record CuriosSlotRef(String identifier, int slotIndex, ItemStack stack) { + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/bag/InventoryBagLocation.java b/src/main/java/com/trunksbomb/bagtabs/bag/InventoryBagLocation.java new file mode 100644 index 0000000..0ed1734 --- /dev/null +++ b/src/main/java/com/trunksbomb/bagtabs/bag/InventoryBagLocation.java @@ -0,0 +1,37 @@ +package com.trunksbomb.bagtabs.bag; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public record InventoryBagLocation(int slot) implements BagLocation { + @Override + public ItemStack getStack(Player player) { + return player.getInventory().getItem(this.slot); + } + + @Override + public boolean setStack(Player player, ItemStack stack) { + player.getInventory().setItem(this.slot, stack); + return true; + } + + @Override + public boolean moveToHotbar(ServerPlayer player, int hotbarSlot) { + Inventory inventory = player.getInventory(); + if (this.slot == hotbarSlot) { + return !inventory.getItem(hotbarSlot).isEmpty(); + } + + ItemStack sourceStack = inventory.getItem(this.slot).copy(); + if (sourceStack.isEmpty()) { + return false; + } + + ItemStack hotbarStack = inventory.getItem(hotbarSlot).copy(); + inventory.setItem(hotbarSlot, sourceStack); + inventory.setItem(this.slot, hotbarStack); + return true; + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index 2c53ca5..0c43a2b 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -581,11 +581,12 @@ public final class BagTabOverlay { } private static boolean supportsTabs(AbstractContainerScreen screen) { - return screen instanceof net.minecraft.client.gui.screens.inventory.InventoryScreen || BagCompat.supportsMenu(screen.getMenu()); + return screen instanceof net.minecraft.client.gui.screens.inventory.InventoryScreen + || BagCompat.supportsMenu(screen.getMenu(), net.minecraft.client.Minecraft.getInstance().player); } private static int getActiveBagSlot(AbstractContainerScreen screen) { - return BagCompat.getActiveBagSlot(screen.getMenu()); + return BagCompat.getActiveBagSlot(screen.getMenu(), net.minecraft.client.Minecraft.getInstance().player); } private static void refreshInsertTargets(AbstractContainerScreen screen, ItemStack carried) { diff --git a/src/main/java/com/trunksbomb/bagtabs/item/BagItem.java b/src/main/java/com/trunksbomb/bagtabs/item/BagItem.java index dd5d0a9..c4096e9 100644 --- a/src/main/java/com/trunksbomb/bagtabs/item/BagItem.java +++ b/src/main/java/com/trunksbomb/bagtabs/item/BagItem.java @@ -3,6 +3,8 @@ 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.BagLocation; +import com.trunksbomb.bagtabs.bag.InventoryBagLocation; import com.trunksbomb.bagtabs.bag.InventoryBag; import com.trunksbomb.bagtabs.menu.BagMenu; import java.util.ArrayList; @@ -61,7 +63,11 @@ public class BagItem extends Item implements InventoryBag { @Override public void openFromInventory(ServerPlayer player, int slot) { - ItemStack stack = player.getInventory().getItem(slot); + this.openFromLocation(player, new InventoryBagLocation(slot), slot); + } + + public void openFromLocation(ServerPlayer player, BagLocation location, int slot) { + ItemStack stack = location.getStack(player); if (!(stack.getItem() instanceof BagItem)) { return; } @@ -73,7 +79,7 @@ public class BagItem extends Item implements InventoryBag { new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu( containerId, playerInventory, - new BagContainer(player, slot), + new BagContainer(player, location), slot ), title), buf -> buf.writeVarInt(slot) diff --git a/src/main/java/com/trunksbomb/bagtabs/menu/BagMenu.java b/src/main/java/com/trunksbomb/bagtabs/menu/BagMenu.java index b94c1b3..421aa8e 100644 --- a/src/main/java/com/trunksbomb/bagtabs/menu/BagMenu.java +++ b/src/main/java/com/trunksbomb/bagtabs/menu/BagMenu.java @@ -1,7 +1,10 @@ package com.trunksbomb.bagtabs.menu; import com.trunksbomb.bagtabs.BagTabs; +import com.trunksbomb.bagtabs.bag.BagAccess; import com.trunksbomb.bagtabs.bag.BagContainer; +import com.trunksbomb.bagtabs.bag.BagEntry; +import com.trunksbomb.bagtabs.bag.InventoryBagLocation; import com.trunksbomb.bagtabs.bag.InventoryBag; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.world.Container; @@ -23,7 +26,13 @@ public class BagMenu extends AbstractContainerMenu { public static BagMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) { int slot = extraData.readVarInt(); - return new BagMenu(containerId, playerInventory, new BagContainer(playerInventory.player, slot), slot); + BagEntry bag = BagAccess.findBag(playerInventory.player, slot); + return new BagMenu( + containerId, + playerInventory, + new BagContainer(playerInventory.player, bag != null ? bag.location() : new InventoryBagLocation(slot)), + slot + ); } public BagMenu(int containerId, Inventory playerInventory, Container container, int bagSlot) { diff --git a/src/main/java/com/trunksbomb/bagtabs/network/InsertIntoBagPayload.java b/src/main/java/com/trunksbomb/bagtabs/network/InsertIntoBagPayload.java index a359f27..b5d131e 100644 --- a/src/main/java/com/trunksbomb/bagtabs/network/InsertIntoBagPayload.java +++ b/src/main/java/com/trunksbomb/bagtabs/network/InsertIntoBagPayload.java @@ -1,6 +1,8 @@ package com.trunksbomb.bagtabs.network; import com.trunksbomb.bagtabs.BagTabs; +import com.trunksbomb.bagtabs.bag.BagAccess; +import com.trunksbomb.bagtabs.bag.BagEntry; import com.trunksbomb.bagtabs.bag.BagCompat; import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; @@ -28,11 +30,12 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload { return; } - if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) { + BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot()); + if (entry == null) { return; } - ItemStack bagStack = serverPlayer.getInventory().getItem(payload.slot()); + ItemStack bagStack = entry.location().getStack(serverPlayer); ItemStack carriedStack = serverPlayer.containerMenu.getCarried(); if (carriedStack.isEmpty() || bagStack.isEmpty()) { return; @@ -42,8 +45,8 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload { return; } - BagCompat.BagHandler handler = BagCompat.findHandler(bagStack); - if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), bagStack, carriedStack)) { + BagCompat.BagHandler handler = entry.handler(); + if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), entry.location(), bagStack, carriedStack)) { serverPlayer.containerMenu.broadcastChanges(); } } diff --git a/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java b/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java index 6052eb4..eef75df 100644 --- a/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java +++ b/src/main/java/com/trunksbomb/bagtabs/network/OpenBagPayload.java @@ -1,6 +1,8 @@ package com.trunksbomb.bagtabs.network; import com.trunksbomb.bagtabs.BagTabs; +import com.trunksbomb.bagtabs.bag.BagAccess; +import com.trunksbomb.bagtabs.bag.BagEntry; import com.trunksbomb.bagtabs.bag.BagCompat; import net.minecraft.core.component.DataComponents; import net.minecraft.network.RegistryFriendlyByteBuf; @@ -43,41 +45,40 @@ public record OpenBagPayload(int slot) implements CustomPacketPayload { return; } - if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) { + BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot()); + if (entry == null) { return; } - ItemStack stack = serverPlayer.getInventory().getItem(payload.slot()); - BagCompat.BagHandler handler = BagCompat.findHandler(stack); + ItemStack stack = entry.location().getStack(serverPlayer); + BagCompat.BagHandler handler = entry.handler(); if (handler != null) { - handler.open(serverPlayer, payload.slot(), stack); + handler.open(serverPlayer, payload.slot(), entry.location(), stack); return; } if (BagCompat.canShowInTabs(stack)) { - if (!openViaHotbarFallback(serverPlayer, payload.slot())) { + if (!openViaHotbarFallback(serverPlayer, entry)) { serverPlayer.sendSystemMessage(BagTabs.translation("generic_open_failed")); BagTabs.LOGGER.debug("Failed to open bag fallback for item {} in slot {}", stack, payload.slot()); } } } - private static boolean openViaHotbarFallback(ServerPlayer player, int sourceSlot) { + private static boolean openViaHotbarFallback(ServerPlayer player, BagEntry entry) { Inventory inventory = player.getInventory(); - ItemStack sourceStack = inventory.getItem(sourceSlot); + ItemStack sourceStack = entry.location().getStack(player); if (sourceStack.isEmpty()) { return false; } - int targetHotbarSlot = chooseHotbarSlot(inventory, sourceSlot); + int targetHotbarSlot = chooseHotbarSlot(inventory, entry.slot()); if (targetHotbarSlot < 0) { return false; } - if (sourceSlot != targetHotbarSlot) { - ItemStack hotbarStack = inventory.getItem(targetHotbarSlot).copy(); - inventory.setItem(targetHotbarSlot, sourceStack.copy()); - inventory.setItem(sourceSlot, hotbarStack); + if (!entry.location().moveToHotbar(player, targetHotbarSlot)) { + return false; } inventory.selected = targetHotbarSlot; diff --git a/src/main/java/com/trunksbomb/bagtabs/network/QueryInsertTargetsPayload.java b/src/main/java/com/trunksbomb/bagtabs/network/QueryInsertTargetsPayload.java index 0436456..092c0c6 100644 --- a/src/main/java/com/trunksbomb/bagtabs/network/QueryInsertTargetsPayload.java +++ b/src/main/java/com/trunksbomb/bagtabs/network/QueryInsertTargetsPayload.java @@ -37,8 +37,11 @@ public record QueryInsertTargetsPayload(int requestId) implements CustomPacketPa if (!carriedStack.isEmpty()) { for (BagEntry bag : BagAccess.findBags(serverPlayer)) { - ItemStack bagStack = serverPlayer.getInventory().getItem(bag.slot()); - if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bagStack, carriedStack)) { + if (bag.handler() == null) { + continue; + } + ItemStack bagStack = bag.location().getStack(serverPlayer); + if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bag.location(), bagStack, carriedStack)) { insertableSlots.add(bag.slot()); } } diff --git a/src/main/resources/data/curios/tags/item/back.json b/src/main/resources/data/curios/tags/item/back.json new file mode 100644 index 0000000..27221bb --- /dev/null +++ b/src/main/resources/data/curios/tags/item/back.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "bagtabs:bag" + ] +} diff --git a/src/main/resources/data/curios/tags/item/belt.json b/src/main/resources/data/curios/tags/item/belt.json new file mode 100644 index 0000000..27221bb --- /dev/null +++ b/src/main/resources/data/curios/tags/item/belt.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "bagtabs:bag" + ] +} diff --git a/src/test/java/com/trunksbomb/bagtabs/api/examples/ExampleBagTabsCompat.java b/src/test/java/com/trunksbomb/bagtabs/api/examples/ExampleBagTabsCompat.java new file mode 100644 index 0000000..29e2137 --- /dev/null +++ b/src/test/java/com/trunksbomb/bagtabs/api/examples/ExampleBagTabsCompat.java @@ -0,0 +1,107 @@ +package com.trunksbomb.bagtabs.api.examples; + +import com.trunksbomb.bagtabs.api.BagTabsApi; +import com.trunksbomb.bagtabs.api.BagTabsIdentity; +import com.trunksbomb.bagtabs.api.BagTabsLocation; +import com.trunksbomb.bagtabs.api.BagTabsProvider; +import java.util.UUID; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; + +/** + * Example-only provider registration that other mod authors can copy and adapt. + * + *

This class is intentionally placed under src/test so it does not ship in the final mod jar. + */ +public final class ExampleBagTabsCompat { + private ExampleBagTabsCompat() { + } + + public static void register() { + BagTabsApi.registerProvider(new ExampleProvider()); + } + + private static final class ExampleProvider implements BagTabsProvider { + @Override + public boolean supports(ItemStack stack) { + return stack.getItem() instanceof ExampleBagItem; + } + + @Override + public BagTabsIdentity getIdentity(ItemStack stack) { + return new BagTabsIdentity("examplemod:bag:" + ExampleBagItem.getOrCreateBagId(stack), "uuid", true); + } + + @Override + public void open(ServerPlayer player, int slot, BagTabsLocation location, ItemStack stack) { + ExampleBagItem.openFromLocation(player, location, slot, stack); + } + + @Override + public boolean isActiveMenu(AbstractContainerMenu menu, Player player, int slot, BagTabsLocation location, ItemStack stack) { + return menu instanceof ExampleBagMenu; + } + + @Override + public float getFullness(ItemStack stack) { + ItemContainerContents contents = stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY); + return contents.equals(ItemContainerContents.EMPTY) ? 0.0F : 1.0F; + } + } + + /** + * Minimal fake bag item used only to demonstrate the API shape. + */ + private static class ExampleBagItem extends Item { + private static final String BAG_ID_KEY = "examplemod-bag-id"; + + private ExampleBagItem() { + super(new Item.Properties().stacksTo(1).component(DataComponents.CONTAINER, ItemContainerContents.EMPTY)); + } + + private static String getOrCreateBagId(ItemStack stack) { + String existing = stack.getOrDefault(DataComponents.CUSTOM_NAME, Component.empty()).getString(); + if (!existing.isBlank()) { + return existing; + } + + String generated = BAG_ID_KEY + "-" + UUID.randomUUID(); + stack.set(DataComponents.CUSTOM_NAME, Component.literal(generated)); + return generated; + } + + private static void openFromLocation(ServerPlayer player, BagTabsLocation location, int slot) { + player.openMenu(new SimpleMenuProvider( + (containerId, inventory, ignored) -> new ExampleBagMenu(containerId, inventory), + Component.literal("Example Bag") + )); + } + } + + /** + * Minimal fake menu used only to demonstrate active-menu reporting. + */ + private static final class ExampleBagMenu extends AbstractContainerMenu { + private ExampleBagMenu(int containerId, Inventory inventory) { + super(null, containerId); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } + + @Override + public boolean stillValid(Player player) { + return true; + } + } +}