Compare commits

3 Commits

34 changed files with 1291 additions and 89 deletions

View File

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

105
API.md Normal file
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

@@ -0,0 +1,327 @@
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$root = Split-Path -Parent $PSScriptRoot
$guiDir = Join-Path $root 'src\main\resources\assets\bagtabs\textures\gui'
$itemDir = Join-Path $root 'src\main\resources\assets\bagtabs\textures\item'
$outDir = Join-Path $root 'art\mockups'
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
function New-Bitmap {
param([int]$Width, [int]$Height)
return [System.Drawing.Bitmap]::new($Width, $Height, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
}
function Load-Bitmap {
param([string]$Path)
return [System.Drawing.Bitmap]::new($Path)
}
function Get-Half {
param(
[System.Drawing.Bitmap]$Image,
[bool]$RightHalf = $false
)
$halfWidth = [int]($Image.Width / 2)
$sourceX = if ($RightHalf) { $halfWidth } else { 0 }
$bmp = New-Bitmap $halfWidth $Image.Height
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
try {
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
$graphics.DrawImage(
$Image,
[System.Drawing.Rectangle]::new(0, 0, $halfWidth, $Image.Height),
[System.Drawing.Rectangle]::new($sourceX, 0, $halfWidth, $Image.Height),
[System.Drawing.GraphicsUnit]::Pixel
)
} finally {
$graphics.Dispose()
}
return $bmp
}
function Tint-Bitmap {
param(
[System.Drawing.Bitmap]$Image,
[Nullable[int]]$Rgb
)
$bmp = New-Bitmap $Image.Width $Image.Height
$tr = if ($Rgb.HasValue) { (($Rgb.Value -shr 16) -band 0xFF) / 255.0 } else { 1.0 }
$tg = if ($Rgb.HasValue) { (($Rgb.Value -shr 8) -band 0xFF) / 255.0 } else { 1.0 }
$tb = if ($Rgb.HasValue) { ($Rgb.Value -band 0xFF) / 255.0 } else { 1.0 }
for ($x = 0; $x -lt $Image.Width; $x++) {
for ($y = 0; $y -lt $Image.Height; $y++) {
$src = $Image.GetPixel($x, $y)
if ($src.A -eq 0) {
$bmp.SetPixel($x, $y, $src)
continue
}
$r = [Math]::Min(255, [int]($src.R * $tr))
$g = [Math]::Min(255, [int]($src.G * $tg))
$b = [Math]::Min(255, [int]($src.B * $tb))
$bmp.SetPixel($x, $y, [System.Drawing.Color]::FromArgb($src.A, $r, $g, $b))
}
}
return $bmp
}
function Compose-Layered {
param(
[System.Drawing.Bitmap]$Base,
[System.Drawing.Bitmap]$Overlay
)
$bmp = New-Bitmap $Base.Width $Base.Height
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
try {
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
$graphics.DrawImage($Base, 0, 0, $Base.Width, $Base.Height)
$graphics.DrawImage($Overlay, 0, 0, $Overlay.Width, $Overlay.Height)
} finally {
$graphics.Dispose()
}
return $bmp
}
function Draw-ScaledBitmap {
param(
[System.Drawing.Graphics]$Graphics,
[System.Drawing.Bitmap]$Bitmap,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height
)
$Graphics.DrawImage(
$Bitmap,
[System.Drawing.Rectangle]::new($X, $Y, $Width, $Height),
[System.Drawing.Rectangle]::new(0, 0, $Bitmap.Width, $Bitmap.Height),
[System.Drawing.GraphicsUnit]::Pixel
)
}
function Draw-Pin {
param([System.Drawing.Graphics]$Graphics, [int]$X, [int]$Y)
$yellow = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 235, 205, 86))
$red = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 205, 55, 44))
try {
$Graphics.FillRectangle($yellow, $X + 1, $Y + 1, 3, 5)
$Graphics.FillRectangle($yellow, $X + 2, $Y + 6, 2, 1)
$Graphics.FillRectangle($red, $X, $Y + 2, 1, 1)
} finally {
$yellow.Dispose()
$red.Dispose()
}
}
function Draw-Fullness {
param(
[System.Drawing.Graphics]$Graphics,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height,
[double]$Percent
)
$back = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 40, 40, 40))
$fill = [System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(255, 105, 180, 90))
try {
$Graphics.FillRectangle($back, $X, $Y, $Width, $Height)
$fillHeight = [Math]::Max(1, [int]([Math]::Round($Height * $Percent)))
$Graphics.FillRectangle($fill, $X, $Y + ($Height - $fillHeight), $Width, $fillHeight)
} finally {
$back.Dispose()
$fill.Dispose()
}
}
function Draw-BagItem {
param(
[System.Drawing.Graphics]$Graphics,
[System.Drawing.Bitmap]$BagBase,
[System.Drawing.Bitmap]$BagOverlay,
[Nullable[int]]$Color,
[int]$X,
[int]$Y,
[int]$Size
)
$tinted = Tint-Bitmap $BagBase $Color
$bag = Compose-Layered $tinted $BagOverlay
try {
Draw-ScaledBitmap $Graphics $bag $X $Y $Size $Size
} finally {
$tinted.Dispose()
$bag.Dispose()
}
}
function Draw-CompactText {
param(
[System.Drawing.Graphics]$Graphics,
[string]$Line1,
[string]$Line2,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height,
[System.Drawing.Color]$Color
)
$font = [System.Drawing.Font]::new('Microsoft Sans Serif', 5.5, [System.Drawing.FontStyle]::Bold, [System.Drawing.GraphicsUnit]::Pixel)
$brush = [System.Drawing.SolidBrush]::new($Color)
$format = [System.Drawing.StringFormat]::new()
$format.Alignment = [System.Drawing.StringAlignment]::Center
$format.LineAlignment = [System.Drawing.StringAlignment]::Center
try {
$Graphics.TextRenderingHint = [System.Drawing.Text.TextRenderingHint]::SingleBitPerPixelGridFit
$Graphics.DrawString($Line1, $font, $brush, [System.Drawing.RectangleF]::new($X, $Y - 1, $Width, [int]($Height / 2)), $format)
$Graphics.DrawString($Line2, $font, $brush, [System.Drawing.RectangleF]::new($X, $Y + [int]($Height / 2) - 2, $Width, [int]($Height / 2) + 2), $format)
} finally {
$font.Dispose()
$brush.Dispose()
$format.Dispose()
}
}
function New-Canvas {
param([int]$Width, [int]$Height)
$bmp = New-Bitmap $Width $Height
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::NearestNeighbor
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::Half
$graphics.Clear([System.Drawing.Color]::FromArgb(255, 48, 48, 52))
return @{ Bitmap = $bmp; Graphics = $graphics }
}
$bagNamer = Load-Bitmap (Join-Path $guiDir 'bag_namer.png')
$tabBase = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_base.png'))
$tabOverlay = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_overlay.png'))
$compactBase = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_compact_base.png'))
$compactOverlay = Get-Half (Load-Bitmap (Join-Path $guiDir 'bag_tabs_compact_overlay.png'))
$dockTab = Load-Bitmap (Join-Path $guiDir 'bag_tabs_dock.png')
$gear = Load-Bitmap (Join-Path $guiDir 'dock_gear.png')
$lock = Load-Bitmap (Join-Path $guiDir 'dock_lock.png')
$prev = Load-Bitmap (Join-Path $guiDir 'dock_prev.png')
$next = Load-Bitmap (Join-Path $guiDir 'dock_next.png')
$bagBase = Load-Bitmap (Join-Path $itemDir 'bag.png')
$bagOverlay = Load-Bitmap (Join-Path $itemDir 'bag_overlay.png')
$bagColors = @($null, 0x8032A8, 0xE4E5DE)
$bagFullness = @(0.82, 0.56, 0.31)
$compactNames = @(
@{ Line1 = 'Canvas'; Line2 = 'Bag' },
@{ Line1 = 'Purple'; Line2 = 'Bag' },
@{ Line1 = 'Pale'; Line2 = 'Bag' }
)
function Draw-StandardStrip {
param(
[System.Drawing.Graphics]$Graphics,
[int]$X,
[int]$Y,
[bool]$Compact
)
Draw-ScaledBitmap $Graphics $dockTab $X $Y 18 22
Draw-ScaledBitmap $Graphics $gear ($X + 5) ($Y + 2) 8 8
Draw-ScaledBitmap $Graphics $lock ($X + 5) ($Y + 12) 8 8
$tabStep = 21
$tabX = $X + 17
for ($i = 0; $i -lt 3; $i++) {
if ($Compact) {
$tintedBase = Tint-Bitmap $compactBase $bagColors[$i]
$tab = Compose-Layered $tintedBase $compactOverlay
try {
Draw-ScaledBitmap $Graphics $tab ($tabX + ($i * $tabStep)) ($Y + 11) 22 11
} finally {
$tintedBase.Dispose()
$tab.Dispose()
}
$textColor = if ($bagColors[$i] -eq $null -or $bagColors[$i] -gt 0x7F7F7F) {
[System.Drawing.Color]::FromArgb(255, 30, 30, 30)
} else {
[System.Drawing.Color]::White
}
Draw-CompactText $Graphics $compactNames[$i].Line1 $compactNames[$i].Line2 ($tabX + ($i * $tabStep) + 2) ($Y + 11) 18 10 $textColor
Draw-Fullness $Graphics ($tabX + ($i * $tabStep) + 18) ($Y + 12) 2 8 $bagFullness[$i]
} else {
$tintedBase = Tint-Bitmap $tabBase $bagColors[$i]
$tab = Compose-Layered $tintedBase $tabOverlay
try {
Draw-ScaledBitmap $Graphics $tab ($tabX + ($i * $tabStep)) $Y 22 22
} finally {
$tintedBase.Dispose()
$tab.Dispose()
}
Draw-BagItem $Graphics $bagBase $bagOverlay $bagColors[$i] ($tabX + ($i * $tabStep) + 3) ($Y + 2) 16
Draw-Fullness $Graphics ($tabX + ($i * $tabStep) + 18) ($Y + 3) 2 15 $bagFullness[$i]
}
}
Draw-Pin $Graphics ($tabX + 1) ($Y + 1)
$scrollX = $tabX + (3 * $tabStep)
Draw-ScaledBitmap $Graphics $dockTab $scrollX $Y 18 22
Draw-ScaledBitmap $Graphics $prev ($scrollX + 5) ($Y + 2) 8 8
Draw-ScaledBitmap $Graphics $next ($scrollX + 5) ($Y + 12) 8 8
}
function Save-Mockup {
param(
[string]$Name,
[bool]$Compact,
[bool]$Floating
)
$canvas = New-Canvas 260 220
$bmp = $canvas.Bitmap
$graphics = $canvas.Graphics
try {
$guiX = 42
$guiY = 28
Draw-ScaledBitmap $graphics $bagNamer $guiX $guiY $bagNamer.Width $bagNamer.Height
if ($Floating) {
$stripX = $guiX + 18
$stripY = $guiY + $bagNamer.Height + 12
} else {
$stripX = $guiX + 2
$stripY = if ($Compact) { $guiY + $bagNamer.Height - 2 } else { $guiY + $bagNamer.Height - 3 }
}
Draw-StandardStrip $graphics $stripX $stripY $Compact
$bmp.Save((Join-Path $outDir $Name), [System.Drawing.Imaging.ImageFormat]::Png)
} finally {
$graphics.Dispose()
$bmp.Dispose()
}
}
Save-Mockup 'rename_docked_normal.png' $false $false
Save-Mockup 'rename_docked_compact.png' $true $false
Save-Mockup 'rename_floating_horizontal_normal.png' $false $true
Save-Mockup 'rename_floating_horizontal_compact.png' $true $true
$bagNamer.Dispose()
$tabBase.Dispose()
$tabOverlay.Dispose()
$compactBase.Dispose()
$compactOverlay.Dispose()
$dockTab.Dispose()
$gear.Dispose()
$lock.Dispose()
$prev.Dispose()
$next.Dispose()
$bagBase.Dispose()
$bagOverlay.Dispose()

View File

@@ -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<BagTabsProvider> 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<BagTabsProvider> getProviders() {
return List.copyOf(PROVIDERS);
}
}

View File

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

View File

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

View File

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

View File

@@ -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<Item> backTag() {
return tagForSlot(BACK_SLOT);
}
public static TagKey<Item> beltTag() {
return tagForSlot(BELT_SLOT);
}
public static TagKey<Item> tagForSlot(String slotIdentifier) {
return ItemTags.create(ResourceLocation.fromNamespaceAndPath("curios", slotIdentifier));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package com.trunksbomb.bagtabs.bag;
import com.trunksbomb.bagtabs.api.BagTabsLocation;
public interface BagLocation extends BagTabsLocation {
}

View File

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

View File

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

View File

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

View File

@@ -49,6 +49,9 @@ public final class BagTabOverlay {
private static final ResourceLocation PREV_ICON_TEXTURE = BagTabs.id("textures/gui/dock_prev.png");
private static final ResourceLocation NEXT_ICON_TEXTURE = BagTabs.id("textures/gui/dock_next.png");
private static final ResourceLocation POPUP_TEXTURE = BagTabs.id("textures/gui/dock_config_popup.png");
private static final ResourceLocation BOOKMARK_TEXTURE = BagTabs.id("textures/gui/bookmark.png");
private static final ResourceLocation NORMAL_FILL_GAUGE_TEXTURE = BagTabs.id("textures/gui/normal_fill_gauge.png");
private static final ResourceLocation NORMAL_ACTIVE_TAB_TEXTURE = BagTabs.id("textures/gui/normal_active_tab.png");
private static final int BASE_TAB_WIDTH = 22;
private static final int BASE_TAB_HEIGHT = 22;
private static final int BASE_COMPACT_TAB_WIDTH = 22;
@@ -59,6 +62,12 @@ public final class BagTabOverlay {
private static final int POPUP_WIDTH = 140;
private static final int POPUP_HEIGHT = 160;
private static final int POPUP_TITLE_HEIGHT = 16;
private static final int BOOKMARK_WIDTH = 3;
private static final int BOOKMARK_HEIGHT = 7;
private static final int NORMAL_FILL_GAUGE_WIDTH = 2;
private static final int NORMAL_FILL_GAUGE_HEIGHT = 18;
private static final int NORMAL_ACTIVE_TAB_WIDTH = 20;
private static final int NORMAL_ACTIVE_TAB_HEIGHT = 4;
private static final int TAB_GAP = 0;
private static final int TAB_Y_OFFSET = -3;
private static final int TAB_X_OFFSET = -6;
@@ -545,31 +554,32 @@ public final class BagTabOverlay {
int uOffset = (hovered || selected) ? baseWidth : 0;
ResourceLocation baseTexture = tab.baseTexture();
ResourceLocation overlayTexture = tab.overlayTexture();
final float tintRed = red;
final float tintGreen = green;
final float tintBlue = blue;
RenderSystem.setShaderColor(red, green, blue, 1.0F);
blitRotated(g, baseTexture, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, baseWidth, baseHeight, baseWidth * 2, baseHeight, tab.rotationDegrees());
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
blitRotated(g, overlayTexture, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, baseWidth, baseHeight, baseWidth * 2, baseHeight, tab.rotationDegrees());
g.pose().pushPose();
pushTabTransform(g, tab);
if (dragged || tab.usesFloatingArt()) {
g.fill(1, 0, baseWidth - 1, 1, 0xFF000000);
if (baseHeight > 2) {
g.fill(2, 1, baseWidth - 2, 2, 0x80FFFFFF);
withTabLocalSpace(g, tab, () -> {
RenderSystem.setShaderColor(tintRed, tintGreen, tintBlue, 1.0F);
blitTabLocalTexture(g, baseTexture, uOffset, 0, baseWidth, baseHeight);
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
blitTabLocalTexture(g, overlayTexture, uOffset, 0, baseWidth, baseHeight);
if (dragged || tab.usesFloatingArt()) {
g.fill(1, 0, baseWidth - 1, 1, 0xFF000000);
if (baseHeight > 2) {
g.fill(2, 1, baseWidth - 2, 2, 0x80FFFFFF);
}
}
}
if (selected) {
renderSelectedIndicator(g, tab, baseWidth, baseHeight);
}
if (!carried.isEmpty()) {
if (INSERTABLE_SLOTS.contains(tab.entry().slot())) {
renderPlusIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
} else {
renderXIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
if (selected) {
renderSelectedIndicator(g, tab, baseWidth, baseHeight);
}
}
g.pose().popPose();
if (!carried.isEmpty()) {
if (INSERTABLE_SLOTS.contains(tab.entry().slot())) {
renderPlusIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
} else {
renderXIndicator(g, baseWidth - 7, Math.min(3, Math.max(0, baseHeight - 6)));
}
}
});
if (tab.showFullnessIndicator() && tab.fullness() >= 0.0F) {
renderFullnessIndicator(g, tab, tab.fullness());
@@ -581,11 +591,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) {
@@ -628,6 +639,11 @@ public final class BagTabOverlay {
}
private static void renderFullnessIndicator(GuiGraphics g, RenderedTab tab, float fullness) {
if (!tab.compact()) {
renderNormalFullnessIndicator(g, tab, fullness);
return;
}
int color = interpolateColor(0xFF65D96D, 0xFFD94A4A, fullness);
if (tab.isVertical()) {
int trackHeight = tab.height() <= 11 ? 1 : 2;
@@ -658,6 +674,64 @@ public final class BagTabOverlay {
g.fill(trackX + trackWidth, trackY, trackX + trackWidth + 1, trackY + trackHeight, 0xFF000000);
}
private static void renderNormalFullnessIndicator(GuiGraphics g, RenderedTab tab, float fullness) {
int color = interpolateColor(0xFF6CCBFF, 0xFF2E86FF, fullness);
withTabLocalSpace(g, tab, () -> {
int usableHeight = NORMAL_FILL_GAUGE_HEIGHT - 2;
int fillHeight = Math.max(0, Math.min(usableHeight, Math.round(usableHeight * fullness)));
if (tab.isVertical()) {
int gaugeX = Math.max(0, (tab.baseTextureWidth() - NORMAL_FILL_GAUGE_HEIGHT) / 2);
int gaugeY = tab.baseTextureHeight() - NORMAL_FILL_GAUGE_WIDTH - 1;
LocalElementTransform transform = LocalElementTransform.identity()
.translate(gaugeX, gaugeY)
.rotate(-90.0F);
withLocalElementTransform(g, 0, 0, NORMAL_FILL_GAUGE_WIDTH, NORMAL_FILL_GAUGE_HEIGHT, transform, () -> {
g.blit(
NORMAL_FILL_GAUGE_TEXTURE,
0,
0,
0,
0,
NORMAL_FILL_GAUGE_WIDTH,
NORMAL_FILL_GAUGE_HEIGHT,
NORMAL_FILL_GAUGE_WIDTH,
NORMAL_FILL_GAUGE_HEIGHT
);
if (fillHeight > 0) {
int fillTop = 1 + (usableHeight - fillHeight);
g.fill(1, fillTop, 2, 1 + usableHeight, color);
}
});
return;
}
int gaugeX = tab.baseTextureWidth() - NORMAL_FILL_GAUGE_WIDTH - 1;
int gaugeY = isFlippedHorizontalShell(tab) ? tab.baseTextureHeight() - NORMAL_FILL_GAUGE_HEIGHT : 0;
LocalElementTransform transform = LocalElementTransform.identity().translate(gaugeX, gaugeY);
if (isFlippedHorizontalShell(tab)) {
transform = transform.mirrorVertical();
}
withLocalElementTransform(g, 0, 0, NORMAL_FILL_GAUGE_WIDTH, NORMAL_FILL_GAUGE_HEIGHT, transform, () -> {
g.blit(
NORMAL_FILL_GAUGE_TEXTURE,
0,
0,
0,
0,
NORMAL_FILL_GAUGE_WIDTH,
NORMAL_FILL_GAUGE_HEIGHT,
NORMAL_FILL_GAUGE_WIDTH,
NORMAL_FILL_GAUGE_HEIGHT
);
if (fillHeight > 0) {
int fillTop = 1 + (usableHeight - fillHeight);
g.fill(1, fillTop, 2, 1 + usableHeight, color);
}
});
});
}
private static int interpolateColor(int start, int end, float t) {
float clamped = Math.max(0.0F, Math.min(1.0F, t));
int sr = (start >> 16) & 0xFF;
@@ -981,18 +1055,53 @@ public final class BagTabOverlay {
if (!tab.pinned()) {
return;
}
int x = tab.x() + 2;
int y = tab.y() + 2;
g.fill(x, y, x + 4, y + 1, 0xFFFFDA6B);
g.fill(x + 1, y + 1, x + 3, y + 4, 0xFFFFDA6B);
g.fill(x + 2, y + 4, x + 3, y + 5, 0xFFD94A4A);
if (!tab.compact()) {
withTabLocalSpace(g, tab, () -> {
LocalElementTransform transform = LocalElementTransform.identity();
if (!tab.isVertical()) {
transform = transform.translate(1, isFlippedHorizontalShell(tab) ? tab.baseTextureHeight() - BOOKMARK_HEIGHT : 0);
if (isFlippedHorizontalShell(tab)) {
transform = transform.mirrorVertical();
}
} else {
transform = transform.translate(1, 0);
}
withLocalElementTransform(g, 0, 0, BOOKMARK_WIDTH, BOOKMARK_HEIGHT, transform, () -> {
g.blit(
BOOKMARK_TEXTURE,
0,
0,
0,
0,
BOOKMARK_WIDTH,
BOOKMARK_HEIGHT,
BOOKMARK_WIDTH,
BOOKMARK_HEIGHT
);
});
});
return;
}
withTabLocalSpace(g, tab, () -> blitScaled(g, BOOKMARK_TEXTURE, 1, 0, 2, 5, 0, 0, BOOKMARK_WIDTH, BOOKMARK_HEIGHT, BOOKMARK_WIDTH, BOOKMARK_HEIGHT));
}
private static void renderSelectedIndicator(GuiGraphics g, RenderedTab tab, int baseWidth, int baseHeight) {
if (!tab.compact()) {
int highlightTop = Math.max(baseHeight - 3, 1);
int highlightBottom = Math.max(baseHeight - 1, highlightTop + 1);
g.fill(3, highlightTop, baseWidth - 3, highlightBottom, 0xFFD94A4A);
int activeX = Math.max(0, (baseWidth - NORMAL_ACTIVE_TAB_WIDTH) / 2);
int activeY = baseHeight - NORMAL_ACTIVE_TAB_HEIGHT + 3;
g.blit(
NORMAL_ACTIVE_TAB_TEXTURE,
activeX,
activeY,
0,
0,
NORMAL_ACTIVE_TAB_WIDTH,
NORMAL_ACTIVE_TAB_HEIGHT,
NORMAL_ACTIVE_TAB_WIDTH,
NORMAL_ACTIVE_TAB_HEIGHT
);
return;
}
@@ -1005,6 +1114,10 @@ public final class BagTabOverlay {
}
}
private static boolean isFlippedHorizontalShell(RenderedTab tab) {
return !tab.isVertical() && Math.round(tab.rotationDegrees()) % 360 != 0;
}
private static void renderCompactLabel(GuiGraphics g, RenderedTab tab) {
Minecraft minecraft = Minecraft.getInstance();
int maxTextWidth = Math.max(8, tab.width() - 4);
@@ -1478,6 +1591,29 @@ public final class BagTabOverlay {
g.pose().popPose();
}
private static void withTabLocalSpace(GuiGraphics g, RenderedTab tab, Runnable draw) {
g.pose().pushPose();
pushTabTransform(g, tab);
draw.run();
g.pose().popPose();
}
private static void blitTabLocalTexture(GuiGraphics g, ResourceLocation texture, int u, int v, int regionWidth, int regionHeight) {
g.blit(texture, 0, 0, u, v, regionWidth, regionHeight, regionWidth * 2, regionHeight);
}
private static void withLocalElementTransform(GuiGraphics g, int x, int y, int width, int height, LocalElementTransform transform, Runnable draw) {
g.pose().pushPose();
g.pose().translate(x + transform.translateX() + (width / 2.0F), y + transform.translateY() + (height / 2.0F), 0.0F);
if (transform.rotationDegrees() != 0.0F) {
g.pose().mulPose(Axis.ZP.rotationDegrees(transform.rotationDegrees()));
}
g.pose().scale(transform.flipHorizontal() ? -1.0F : 1.0F, transform.flipVertical() ? -1.0F : 1.0F, 1.0F);
g.pose().translate(-(width / 2.0F), -(height / 2.0F), 0.0F);
draw.run();
g.pose().popPose();
}
private static void pushTabTransform(GuiGraphics g, RenderedTab tab) {
g.pose().translate(tab.x() + (tab.width() / 2.0F), tab.y() + (tab.height() / 2.0F), 0.0F);
g.pose().mulPose(Axis.ZP.rotationDegrees(tab.rotationDegrees()));
@@ -1621,6 +1757,28 @@ public final class BagTabOverlay {
}
}
private record LocalElementTransform(int translateX, int translateY, boolean flipHorizontal, boolean flipVertical, float rotationDegrees) {
private static LocalElementTransform identity() {
return new LocalElementTransform(0, 0, false, false, 0.0F);
}
private LocalElementTransform translate(int x, int y) {
return new LocalElementTransform(this.translateX + x, this.translateY + y, this.flipHorizontal, this.flipVertical, this.rotationDegrees);
}
private LocalElementTransform mirrorHorizontal() {
return new LocalElementTransform(this.translateX, this.translateY, !this.flipHorizontal, this.flipVertical, this.rotationDegrees);
}
private LocalElementTransform mirrorVertical() {
return new LocalElementTransform(this.translateX, this.translateY, this.flipHorizontal, !this.flipVertical, this.rotationDegrees);
}
private LocalElementTransform rotate(float degrees) {
return new LocalElementTransform(this.translateX, this.translateY, this.flipHorizontal, this.flipVertical, this.rotationDegrees + degrees);
}
}
private record PendingClick(int slot, double grabOffsetX, double grabOffsetY, boolean pinned) {
}

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

View File

@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"bagtabs:bag"
]
}

View File

@@ -0,0 +1,6 @@
{
"replace": false,
"values": [
"bagtabs:bag"
]
}

View File

@@ -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.
*
* <p>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;
}
}
}