Compare commits

...

16 Commits

Author SHA1 Message Date
trunksbomb
bc621d456e working on localized tab drawing and then committing that to the screen to help with these weird quirks. 2026-03-23 18:11:17 -04:00
trunksbomb
cfcd591628 still struggling to get the placement right, just need to commit for right now 2026-03-23 16:33:48 -04:00
trunksbomb
e395e87c53 Implement Bag Tab API so that other mods can make themselves first-class compatible with this mod, no guesswork required (but also the API isn't required to at least get some support for most bag-style items)
Implement Curios support
2026-03-23 12:50:49 -04:00
trunksbomb
5c5c35871d ready for initial release
Some checks failed
Release / publish (release) Has been cancelled
2026-03-23 10:58:48 -04:00
trunksbomb
5d283909db first pass on README
Some checks failed
Build / build (push) Has been cancelled
2026-03-23 05:01:55 -04:00
trunksbomb
8189359e91 add a "not fully supported" tooltip for bags that use the fallback hotbar method 2026-03-23 04:44:38 -04:00
trunksbomb
a90227ff02 remove Bag of Holding references
Some checks failed
Build / build (push) Has been cancelled
2026-03-23 04:02:46 -04:00
trunksbomb
6271417381 add support for non-supported bags/packs. If there is no compat layer yet for a bag/pack item from a mod, then attempt to place that bag into your hotbar and simulate a right click to open it. Prefer empty slots, non-tool slots, non-food slots if it needs to swap for something currently on your hotbar. 2026-03-23 03:43:29 -04:00
trunksbomb
274e4f6dec fullness indicator optional on tabs 2026-03-23 03:04:58 -04:00
trunksbomb
e201c8c674 Transform the dock settings GUI into a proper popup so you can live position the dock and make changes. 2026-03-23 02:45:01 -04:00
trunksbomb
966e199a23 UI polishes
add compact mode for all orientations
2026-03-23 01:55:51 -04:00
trunksbomb
56fcb49f4d UI polishes
Implement scrolling tabs
2026-03-23 00:41:46 -04:00
trunksbomb
638fce4116 tidying up UI and UX.
Add screen dock modes (left/right/top/bottom), better X/Y offset control, replaced tab size with "max tabs"
2026-03-22 23:46:35 -04:00
trunksbomb
b51b41206c tidying up UI and UX
Add dock tab for locking the dock and opening settings, where menu dock location and offset can be moved.
2026-03-22 21:44:01 -04:00
trunksbomb
85a7e1a48b reorder pinned bags 2026-03-22 20:30:54 -04:00
trunksbomb
15be03e055 pin bags 2026-03-22 20:10:32 -04:00
59 changed files with 6623 additions and 177 deletions

View File

@@ -0,0 +1,57 @@
name: Release
on:
release:
types:
- published
jobs:
publish:
# 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 }}
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
MODRINTH_PROJECT_ID: ${{ vars.MODRINTH_PROJECT_ID }}
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
MINECRAFT_VERSIONS: ${{ vars.MINECRAFT_VERSIONS }}
MOD_LOADERS: ${{ vars.MOD_LOADERS }}
CURSEFORGE_GAME_VERSION_NAMES: ${{ vars.CURSEFORGE_GAME_VERSION_NAMES }}
CURSEFORGE_GAME_VERSION_IDS: ${{ vars.CURSEFORGE_GAME_VERSION_IDS }}
RELEASE_ARTIFACT_GLOB: ${{ vars.RELEASE_ARTIFACT_GLOB }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build release jar
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
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.

113
README.md
View File

@@ -1,25 +1,100 @@
# Bag Tabs
Installation information Bag Tabs is a NeoForge mod for Minecraft 1.21.1 that adds a tab strip to inventory screens for bags and other portable storage items you are carrying.
=======
This template repository can be directly cloned to get you started with a new The goal is simple: if you are carrying a bag, you should be able to open it directly from your inventory without moving it into your hand first.
mod. Simply create a new repository cloned from this one, by following the
instructions provided by [GitHub](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
Once you have your clone, simply open the repository in the IDE of your choice. The usual recommendation for an IDE is either IntelliJ IDEA or Eclipse. ## What It Does
If at any point you are missing libraries in your IDE, or you've run into problems you can - Adds a tab bar to supported inventory screens.
run `gradlew --refresh-dependencies` to refresh the local cache. `gradlew clean` to reset everything - Shows one tab for each carried bag or portable storage item the mod recognizes.
{this does not affect your code} and then start the process again. - 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.
- Supports drag-and-drop insertion onto supported bag tabs when the bag can accept the carried stack.
Mapping Names: ## Included Content
============
By default, the MDK is configured to use the official mapping names from Mojang for methods and fields
in the Minecraft codebase. These names are covered by a specific license. All modders should be aware of this
license. For the latest license text, refer to the mapping file itself, or the reference copy here:
https://github.com/NeoForged/NeoForm/blob/main/Mojang.md
Additional Resources: - A built-in Bag Tabs bag item.
========== - 27 inventory slots, like a single chest.
Community Documentation: https://docs.neoforged.net/ - Dyeable bag colors.
NeoForged Discord: https://discord.neoforged.net/ - 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.
## Tab Features
- Bag tabs use the bag item icon for normal mode.
- Compact mode replaces the icon with the bag name.
- Dyed bags tint their tab background.
- Pinned tabs always appear first.
- Pinned tabs can be reordered by drag-and-drop.
- Tabs can be locked to disable pinning and reordering.
- Overflow arrows appear automatically when more tabs exist than can be shown.
- Mouse wheel scrolling works over the dock.
- Fullness indicators can be shown on tabs and toggled in dock settings.
## Dock Features
- Dock to the GUI bottom, top, left, or right.
- Dock to the screen bottom, top, left, or right.
- Floating horizontal and floating vertical modes.
- Per-player, per-world saved settings.
- Global default settings plus per-screen overrides.
- Remembered scroll position per screen.
- Adjustable X/Y offset.
- Adjustable max visible tabs.
- Optional compact layout.
- Optional fullness indicator.
- Draggable in-game dock settings popup with live preview.
## Bag Opening Behavior
Bag Tabs supports two kinds of bag opening:
- Direct support for bags with native or compatibility-backed integration.
- A fallback hotbar method for unsupported portable storage items that still look like bags or containers.
When a bag is not fully supported, the tooltip says:
- `Mod not supported: uses hotbar method to open`
If no warning is shown, the bag is using native or compatibility-backed support.
## Current Compatibility
Bag Tabs currently includes compatibility support for:
- Bag Tabs bags
- 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.
## Bag Namer
Bag Tabs includes a Bag Namer tool that can rename portable storage items.
- Works on supported bags and inventory-like items.
- Uses an in-world style GUI with input and output slots.
- Does not close after each rename, so items can be renamed in batches.
- Drops its contents if you leave the screen.
## Controls
- Left-click tab: open bag
- Right-click tab: pin or unpin
- Drag pinned tab: reorder pinned tabs
- Mouse wheel over dock: scroll visible tabs
- `\` by default: open last bag
## Notes
- 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" includeGroup "curse.maven"
} }
} }
maven {
url = uri("https://maven.theillusivec4.top/")
}
} }
base { base {
@@ -124,7 +127,9 @@ configurations {
dependencies { dependencies {
// Dev-runtime helpers for the client. // 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" 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:jade-324717:6155158"
localRuntime "curse.maven:jei-238222:7391682" localRuntime "curse.maven:jei-238222:7391682"
localRuntime "curse.maven:travelers-backpack-321117:7485008" localRuntime "curse.maven:travelers-backpack-321117:7485008"

View File

@@ -30,9 +30,9 @@ mod_id=bagtabs
# The human-readable display name for the mod. # The human-readable display name for the mod.
mod_name=Bag Tabs mod_name=Bag Tabs
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved mod_license=MIT
# The mod version. See https://semver.org/ # The mod version. See https://semver.org/
mod_version=1.0.0 mod_version=0.1.0
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@@ -0,0 +1,767 @@
package net.minecraft.client.gui.screens.inventory;
import com.google.common.collect.Sets;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.datafixers.util.Pair;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.api.distmarker.OnlyIn;
@OnlyIn(Dist.CLIENT)
public abstract class AbstractContainerScreen<T extends AbstractContainerMenu> extends Screen implements MenuAccess<T> {
/**
* The location of the inventory background texture
*/
public static final ResourceLocation INVENTORY_LOCATION = ResourceLocation.withDefaultNamespace("textures/gui/container/inventory.png");
private static final float SNAPBACK_SPEED = 100.0F;
private static final int QUICKDROP_DELAY = 500;
public static final int SLOT_ITEM_BLIT_OFFSET = 100;
private static final int HOVER_ITEM_BLIT_OFFSET = 200;
/**
* The X size of the inventory window in pixels.
*/
protected int imageWidth = 176;
/**
* The Y size of the inventory window in pixels.
*/
protected int imageHeight = 166;
protected int titleLabelX;
protected int titleLabelY;
protected int inventoryLabelX;
protected int inventoryLabelY;
/**
* A list of the players inventory slots
*/
protected final T menu;
protected final Component playerInventoryTitle;
/**
* Holds the slot currently hovered
*/
@Nullable
protected Slot hoveredSlot;
/**
* Used when touchscreen is enabled
*/
@Nullable
private Slot clickedSlot;
@Nullable
private Slot snapbackEnd;
@Nullable
private Slot quickdropSlot;
@Nullable
private Slot lastClickSlot;
/**
* Starting X position for the Gui. Inconsistent use for Gui backgrounds.
*/
protected int leftPos;
/**
* Starting Y position for the Gui. Inconsistent use for Gui backgrounds.
*/
protected int topPos;
/**
* Used when touchscreen is enabled.
*/
private boolean isSplittingStack;
/**
* Used when touchscreen is enabled
*/
private ItemStack draggingItem = ItemStack.EMPTY;
private int snapbackStartX;
private int snapbackStartY;
private long snapbackTime;
/**
* Used when touchscreen is enabled
*/
private ItemStack snapbackItem = ItemStack.EMPTY;
private long quickdropTime;
protected final Set<Slot> quickCraftSlots = Sets.newHashSet();
protected boolean isQuickCrafting;
private int quickCraftingType;
private int quickCraftingButton;
private boolean skipNextRelease;
private int quickCraftingRemainder;
private long lastClickTime;
private int lastClickButton;
private boolean doubleclick;
private ItemStack lastQuickMoved = ItemStack.EMPTY;
public AbstractContainerScreen(T menu, Inventory playerInventory, Component title) {
super(title);
this.menu = menu;
this.playerInventoryTitle = playerInventory.getDisplayName();
this.skipNextRelease = true;
this.titleLabelX = 8;
this.titleLabelY = 6;
this.inventoryLabelX = 8;
this.inventoryLabelY = this.imageHeight - 94;
}
@Override
protected void init() {
this.leftPos = (this.width - this.imageWidth) / 2;
this.topPos = (this.height - this.imageHeight) / 2;
}
/**
* Renders the graphical user interface (GUI) element.
*
* @param guiGraphics the GuiGraphics object used for rendering.
* @param mouseX the x-coordinate of the mouse cursor.
* @param mouseY the y-coordinate of the mouse cursor.
* @param partialTick the partial tick time.
*/
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
int i = this.leftPos;
int j = this.topPos;
// Neo: replicate the super method's implementation to insert the event between background and widgets
this.renderBackground(guiGraphics, mouseX, mouseY, partialTick);
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.post(new net.neoforged.neoforge.client.event.ContainerScreenEvent.Render.Background(this, guiGraphics, mouseX, mouseY));
for (net.minecraft.client.gui.components.Renderable renderable : this.renderables) {
renderable.render(guiGraphics, mouseX, mouseY, partialTick);
}
RenderSystem.disableDepthTest();
guiGraphics.pose().pushPose();
guiGraphics.pose().translate((float)i, (float)j, 0.0F);
this.hoveredSlot = null;
for (int k = 0; k < this.menu.slots.size(); k++) {
Slot slot = this.menu.slots.get(k);
if (slot.isActive()) {
this.renderSlot(guiGraphics, slot);
}
if (this.isHovering(slot, (double)mouseX, (double)mouseY) && slot.isActive()) {
this.hoveredSlot = slot;
this.renderSlotHighlight(guiGraphics, slot, mouseX, mouseY, partialTick);
}
}
this.renderLabels(guiGraphics, mouseX, mouseY);
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.post(new net.neoforged.neoforge.client.event.ContainerScreenEvent.Render.Foreground(this, guiGraphics, mouseX, mouseY));
ItemStack itemstack = this.draggingItem.isEmpty() ? this.menu.getCarried() : this.draggingItem;
if (!itemstack.isEmpty()) {
int l1 = 8;
int i2 = this.draggingItem.isEmpty() ? 8 : 16;
String s = null;
if (!this.draggingItem.isEmpty() && this.isSplittingStack) {
itemstack = itemstack.copyWithCount(Mth.ceil((float)itemstack.getCount() / 2.0F));
} else if (this.isQuickCrafting && this.quickCraftSlots.size() > 1) {
itemstack = itemstack.copyWithCount(this.quickCraftingRemainder);
if (itemstack.isEmpty()) {
s = ChatFormatting.YELLOW + "0";
}
}
this.renderFloatingItem(guiGraphics, itemstack, mouseX - i - 8, mouseY - j - i2, s);
}
if (!this.snapbackItem.isEmpty()) {
float f = (float)(Util.getMillis() - this.snapbackTime) / 100.0F;
if (f >= 1.0F) {
f = 1.0F;
this.snapbackItem = ItemStack.EMPTY;
}
int j2 = this.snapbackEnd.x - this.snapbackStartX;
int k2 = this.snapbackEnd.y - this.snapbackStartY;
int j1 = this.snapbackStartX + (int)((float)j2 * f);
int k1 = this.snapbackStartY + (int)((float)k2 * f);
this.renderFloatingItem(guiGraphics, this.snapbackItem, j1, k1, null);
}
guiGraphics.pose().popPose();
RenderSystem.enableDepthTest();
}
@Override
public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
this.renderTransparentBackground(guiGraphics);
this.renderBg(guiGraphics, partialTick, mouseX, mouseY);
}
public static void renderSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) {
renderSlotHighlight(guiGraphics, x, y, blitOffset, -2130706433);
}
public static void renderSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset, int color) {
guiGraphics.fillGradient(RenderType.guiOverlay(), x, y, x + 16, y + 16, color, color, blitOffset);
}
/**
* Renders a highlight for the given slot to indicate the mouse is currently hovering over it.
*/
protected void renderSlotHighlight(GuiGraphics guiGraphics, Slot slot, int mouseX, int mouseY, float partialTick) {
if (slot.isHighlightable()) {
renderSlotHighlight(guiGraphics, slot.x, slot.y, 0, getSlotColor(slot.index));
}
}
protected void renderTooltip(GuiGraphics guiGraphics, int x, int y) {
if (this.menu.getCarried().isEmpty() && this.hoveredSlot != null && this.hoveredSlot.hasItem()) {
ItemStack itemstack = this.hoveredSlot.getItem();
guiGraphics.renderTooltip(this.font, this.getTooltipFromContainerItem(itemstack), itemstack.getTooltipImage(), itemstack, x, y);
}
}
protected List<Component> getTooltipFromContainerItem(ItemStack stack) {
return getTooltipFromItem(this.minecraft, stack);
}
private void renderFloatingItem(GuiGraphics guiGraphics, ItemStack stack, int x, int y, String text) {
guiGraphics.pose().pushPose();
guiGraphics.pose().translate(0.0F, 0.0F, 232.0F);
guiGraphics.renderItem(stack, x, y);
var font = net.neoforged.neoforge.client.extensions.common.IClientItemExtensions.of(stack).getFont(stack, net.neoforged.neoforge.client.extensions.common.IClientItemExtensions.FontContext.ITEM_COUNT);
guiGraphics.renderItemDecorations(font == null ? this.font : font, stack, x, y - (this.draggingItem.isEmpty() ? 0 : 8), text);
guiGraphics.pose().popPose();
}
protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) {
guiGraphics.drawString(this.font, this.title, this.titleLabelX, this.titleLabelY, 4210752, false);
guiGraphics.drawString(this.font, this.playerInventoryTitle, this.inventoryLabelX, this.inventoryLabelY, 4210752, false);
}
protected abstract void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY);
protected void renderSlot(GuiGraphics guiGraphics, Slot slot) {
int i = slot.x;
int j = slot.y;
ItemStack itemstack = slot.getItem();
boolean flag = false;
boolean flag1 = slot == this.clickedSlot && !this.draggingItem.isEmpty() && !this.isSplittingStack;
ItemStack itemstack1 = this.menu.getCarried();
String s = null;
if (slot == this.clickedSlot && !this.draggingItem.isEmpty() && this.isSplittingStack && !itemstack.isEmpty()) {
itemstack = itemstack.copyWithCount(itemstack.getCount() / 2);
} else if (this.isQuickCrafting && this.quickCraftSlots.contains(slot) && !itemstack1.isEmpty()) {
if (this.quickCraftSlots.size() == 1) {
return;
}
if (AbstractContainerMenu.canItemQuickReplace(slot, itemstack1, true) && this.menu.canDragTo(slot)) {
flag = true;
int k = Math.min(itemstack1.getMaxStackSize(), slot.getMaxStackSize(itemstack1));
int l = slot.getItem().isEmpty() ? 0 : slot.getItem().getCount();
int i1 = AbstractContainerMenu.getQuickCraftPlaceCount(this.quickCraftSlots, this.quickCraftingType, itemstack1) + l;
if (i1 > k) {
i1 = k;
s = ChatFormatting.YELLOW.toString() + k;
}
itemstack = itemstack1.copyWithCount(i1);
} else {
this.quickCraftSlots.remove(slot);
this.recalculateQuickCraftRemaining();
}
}
guiGraphics.pose().pushPose();
guiGraphics.pose().translate(0.0F, 0.0F, 100.0F);
if (itemstack.isEmpty() && slot.isActive()) {
Pair<ResourceLocation, ResourceLocation> pair = slot.getNoItemIcon();
if (pair != null) {
TextureAtlasSprite textureatlassprite = this.minecraft.getTextureAtlas(pair.getFirst()).apply(pair.getSecond());
guiGraphics.blit(i, j, 0, 16, 16, textureatlassprite);
flag1 = true;
}
}
if (!flag1) {
if (flag) {
guiGraphics.fill(i, j, i + 16, j + 16, -2130706433);
}
renderSlotContents(guiGraphics, itemstack, slot, s);
}
guiGraphics.pose().popPose();
}
protected void renderSlotContents(GuiGraphics guiGraphics, ItemStack itemstack, Slot slot, @Nullable String countString) {
GuiGraphics p_281607_ = guiGraphics; Slot p_282613_ = slot; String s = countString; int i = slot.x; int j = slot.y;
int j1 = p_282613_.x + p_282613_.y * this.imageWidth;
if (p_282613_.isFake()) {
p_281607_.renderFakeItem(itemstack, i, j, j1);
} else {
p_281607_.renderItem(itemstack, i, j, j1);
}
p_281607_.renderItemDecorations(this.font, itemstack, i, j, s);
}
private void recalculateQuickCraftRemaining() {
ItemStack itemstack = this.menu.getCarried();
if (!itemstack.isEmpty() && this.isQuickCrafting) {
if (this.quickCraftingType == 2) {
this.quickCraftingRemainder = itemstack.getMaxStackSize();
} else {
this.quickCraftingRemainder = itemstack.getCount();
for (Slot slot : this.quickCraftSlots) {
ItemStack itemstack1 = slot.getItem();
int i = itemstack1.isEmpty() ? 0 : itemstack1.getCount();
int j = Math.min(itemstack.getMaxStackSize(), slot.getMaxStackSize(itemstack));
int k = Math.min(AbstractContainerMenu.getQuickCraftPlaceCount(this.quickCraftSlots, this.quickCraftingType, itemstack) + i, j);
this.quickCraftingRemainder -= k - i;
}
}
}
}
@Nullable
private Slot findSlot(double mouseX, double mouseY) {
for (int i = 0; i < this.menu.slots.size(); i++) {
Slot slot = this.menu.slots.get(i);
if (this.isHovering(slot, mouseX, mouseY) && slot.isActive()) {
return slot;
}
}
return null;
}
/**
* Called when a mouse button is clicked within the GUI element.
* <p>
* @return {@code true} if the event is consumed, {@code false} otherwise.
*
* @param mouseX the X coordinate of the mouse.
* @param mouseY the Y coordinate of the mouse.
* @param button the button that was clicked.
*/
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (super.mouseClicked(mouseX, mouseY, button)) {
return true;
} else {
InputConstants.Key mouseKey = InputConstants.Type.MOUSE.getOrCreate(button);
boolean flag = this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey);
Slot slot = this.findSlot(mouseX, mouseY);
long i = Util.getMillis();
this.doubleclick = this.lastClickSlot == slot && i - this.lastClickTime < 250L && this.lastClickButton == button;
this.skipNextRelease = false;
if (button != 0 && button != 1 && !flag) {
this.checkHotbarMouseClicked(button);
} else {
int j = this.leftPos;
int k = this.topPos;
boolean flag1 = this.hasClickedOutside(mouseX, mouseY, j, k, button);
if (slot != null) flag1 = false; // Forge, prevent dropping of items through slots outside of GUI boundaries
int l = -1;
if (slot != null) {
l = slot.index;
}
if (flag1) {
l = -999;
}
if (this.minecraft.options.touchscreen().get() && flag1 && this.menu.getCarried().isEmpty()) {
this.onClose();
return true;
}
if (l != -1) {
if (this.minecraft.options.touchscreen().get()) {
if (slot != null && slot.hasItem()) {
this.clickedSlot = slot;
this.draggingItem = ItemStack.EMPTY;
this.isSplittingStack = button == 1;
} else {
this.clickedSlot = null;
}
} else if (!this.isQuickCrafting) {
if (this.menu.getCarried().isEmpty()) {
if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) {
this.slotClicked(slot, l, button, ClickType.CLONE);
} else {
boolean flag2 = l != -999
&& (
InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 340)
|| InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 344)
);
ClickType clicktype = ClickType.PICKUP;
if (flag2) {
this.lastQuickMoved = slot != null && slot.hasItem() ? slot.getItem().copy() : ItemStack.EMPTY;
clicktype = ClickType.QUICK_MOVE;
} else if (l == -999) {
clicktype = ClickType.THROW;
}
this.slotClicked(slot, l, button, clicktype);
}
this.skipNextRelease = true;
} else {
this.isQuickCrafting = true;
this.quickCraftingButton = button;
this.quickCraftSlots.clear();
if (button == 0) {
this.quickCraftingType = 0;
} else if (button == 1) {
this.quickCraftingType = 1;
} else if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) {
this.quickCraftingType = 2;
}
}
}
}
}
this.lastClickSlot = slot;
this.lastClickTime = i;
this.lastClickButton = button;
return true;
}
}
private void checkHotbarMouseClicked(int keyCode) {
if (this.hoveredSlot != null && this.menu.getCarried().isEmpty()) {
if (this.minecraft.options.keySwapOffhand.matchesMouse(keyCode)) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 40, ClickType.SWAP);
return;
}
for (int i = 0; i < 9; i++) {
if (this.minecraft.options.keyHotbarSlots[i].matchesMouse(keyCode)) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, i, ClickType.SWAP);
}
}
}
}
protected boolean hasClickedOutside(double mouseX, double mouseY, int guiLeft, int guiTop, int mouseButton) {
return mouseX < (double)guiLeft
|| mouseY < (double)guiTop
|| mouseX >= (double)(guiLeft + this.imageWidth)
|| mouseY >= (double)(guiTop + this.imageHeight);
}
/**
* Called when the mouse is dragged within the GUI element.
* <p>
* @return {@code true} if the event is consumed, {@code false} otherwise.
*
* @param mouseX the X coordinate of the mouse.
* @param mouseY the Y coordinate of the mouse.
* @param button the button that is being dragged.
* @param dragX the X distance of the drag.
* @param dragY the Y distance of the drag.
*/
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
Slot slot = this.findSlot(mouseX, mouseY);
ItemStack itemstack = this.menu.getCarried();
if (this.clickedSlot != null && this.minecraft.options.touchscreen().get()) {
if (button == 0 || button == 1) {
if (this.draggingItem.isEmpty()) {
if (slot != this.clickedSlot && !this.clickedSlot.getItem().isEmpty()) {
this.draggingItem = this.clickedSlot.getItem().copy();
}
} else if (this.draggingItem.getCount() > 1 && slot != null && AbstractContainerMenu.canItemQuickReplace(slot, this.draggingItem, false)) {
long i = Util.getMillis();
if (this.quickdropSlot == slot) {
if (i - this.quickdropTime > 500L) {
this.slotClicked(this.clickedSlot, this.clickedSlot.index, 0, ClickType.PICKUP);
this.slotClicked(slot, slot.index, 1, ClickType.PICKUP);
this.slotClicked(this.clickedSlot, this.clickedSlot.index, 0, ClickType.PICKUP);
this.quickdropTime = i + 750L;
this.draggingItem.shrink(1);
}
} else {
this.quickdropSlot = slot;
this.quickdropTime = i;
}
}
}
} else if (this.isQuickCrafting
&& slot != null
&& !itemstack.isEmpty()
&& (itemstack.getCount() > this.quickCraftSlots.size() || this.quickCraftingType == 2)
&& AbstractContainerMenu.canItemQuickReplace(slot, itemstack, true)
&& slot.mayPlace(itemstack)
&& this.menu.canDragTo(slot)) {
this.quickCraftSlots.add(slot);
this.recalculateQuickCraftRemaining();
}
return true;
}
/**
* Called when a mouse button is released within the GUI element.
* <p>
* @return {@code true} if the event is consumed, {@code false} otherwise.
*
* @param mouseX the X coordinate of the mouse.
* @param mouseY the Y coordinate of the mouse.
* @param button the button that was released.
*/
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
super.mouseReleased(mouseX, mouseY, button); //Forge, Call parent to release buttons
Slot slot = this.findSlot(mouseX, mouseY);
int i = this.leftPos;
int j = this.topPos;
boolean flag = this.hasClickedOutside(mouseX, mouseY, i, j, button);
if (slot != null) flag = false; // Forge, prevent dropping of items through slots outside of GUI boundaries
InputConstants.Key mouseKey = InputConstants.Type.MOUSE.getOrCreate(button);
int k = -1;
if (slot != null) {
k = slot.index;
}
if (flag) {
k = -999;
}
if (this.doubleclick && slot != null && button == 0 && this.menu.canTakeItemForPickAll(ItemStack.EMPTY, slot)) {
if (hasShiftDown()) {
if (!this.lastQuickMoved.isEmpty()) {
for (Slot slot2 : this.menu.slots) {
if (slot2 != null
&& slot2.mayPickup(this.minecraft.player)
&& slot2.hasItem()
&& slot2.isSameInventory(slot)
&& AbstractContainerMenu.canItemQuickReplace(slot2, this.lastQuickMoved, true)) {
this.slotClicked(slot2, slot2.index, button, ClickType.QUICK_MOVE);
}
}
}
} else {
this.slotClicked(slot, k, button, ClickType.PICKUP_ALL);
}
this.doubleclick = false;
this.lastClickTime = 0L;
} else {
if (this.isQuickCrafting && this.quickCraftingButton != button) {
this.isQuickCrafting = false;
this.quickCraftSlots.clear();
this.skipNextRelease = true;
return true;
}
if (this.skipNextRelease) {
this.skipNextRelease = false;
return true;
}
if (this.clickedSlot != null && this.minecraft.options.touchscreen().get()) {
if (button == 0 || button == 1) {
if (this.draggingItem.isEmpty() && slot != this.clickedSlot) {
this.draggingItem = this.clickedSlot.getItem();
}
boolean flag2 = AbstractContainerMenu.canItemQuickReplace(slot, this.draggingItem, false);
if (k != -1 && !this.draggingItem.isEmpty() && flag2) {
this.slotClicked(this.clickedSlot, this.clickedSlot.index, button, ClickType.PICKUP);
this.slotClicked(slot, k, 0, ClickType.PICKUP);
if (this.menu.getCarried().isEmpty()) {
this.snapbackItem = ItemStack.EMPTY;
} else {
this.slotClicked(this.clickedSlot, this.clickedSlot.index, button, ClickType.PICKUP);
this.snapbackStartX = Mth.floor(mouseX - (double)i);
this.snapbackStartY = Mth.floor(mouseY - (double)j);
this.snapbackEnd = this.clickedSlot;
this.snapbackItem = this.draggingItem;
this.snapbackTime = Util.getMillis();
}
} else if (!this.draggingItem.isEmpty()) {
this.snapbackStartX = Mth.floor(mouseX - (double)i);
this.snapbackStartY = Mth.floor(mouseY - (double)j);
this.snapbackEnd = this.clickedSlot;
this.snapbackItem = this.draggingItem;
this.snapbackTime = Util.getMillis();
}
this.clearDraggingState();
}
} else if (this.isQuickCrafting && !this.quickCraftSlots.isEmpty()) {
this.slotClicked(null, -999, AbstractContainerMenu.getQuickcraftMask(0, this.quickCraftingType), ClickType.QUICK_CRAFT);
for (Slot slot1 : this.quickCraftSlots) {
this.slotClicked(slot1, slot1.index, AbstractContainerMenu.getQuickcraftMask(1, this.quickCraftingType), ClickType.QUICK_CRAFT);
}
this.slotClicked(null, -999, AbstractContainerMenu.getQuickcraftMask(2, this.quickCraftingType), ClickType.QUICK_CRAFT);
} else if (!this.menu.getCarried().isEmpty()) {
if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) {
this.slotClicked(slot, k, button, ClickType.CLONE);
} else {
boolean flag1 = k != -999
&& (
InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 340)
|| InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 344)
);
if (flag1) {
this.lastQuickMoved = slot != null && slot.hasItem() ? slot.getItem().copy() : ItemStack.EMPTY;
}
this.slotClicked(slot, k, button, flag1 ? ClickType.QUICK_MOVE : ClickType.PICKUP);
}
}
}
if (this.menu.getCarried().isEmpty()) {
this.lastClickTime = 0L;
}
this.isQuickCrafting = false;
return true;
}
public void clearDraggingState() {
this.draggingItem = ItemStack.EMPTY;
this.clickedSlot = null;
}
private boolean isHovering(Slot slot, double mouseX, double mouseY) {
return this.isHovering(slot.x, slot.y, 16, 16, mouseX, mouseY);
}
protected boolean isHovering(int x, int y, int width, int height, double mouseX, double mouseY) {
int i = this.leftPos;
int j = this.topPos;
mouseX -= (double)i;
mouseY -= (double)j;
return mouseX >= (double)(x - 1)
&& mouseX < (double)(x + width + 1)
&& mouseY >= (double)(y - 1)
&& mouseY < (double)(y + height + 1);
}
/**
* Called when the mouse is clicked over a slot or outside the gui.
*/
protected void slotClicked(Slot slot, int slotId, int mouseButton, ClickType type) {
if (slot != null) {
slotId = slot.index;
}
this.minecraft.gameMode.handleInventoryMouseClick(this.menu.containerId, slotId, mouseButton, type, this.minecraft.player);
}
protected void handleSlotStateChanged(int slotId, int containerId, boolean newState) {
this.minecraft.gameMode.handleSlotStateChanged(slotId, containerId, newState);
}
/**
* Called when a keyboard key is pressed within the GUI element.
* <p>
* @return {@code true} if the event is consumed, {@code false} otherwise.
*
* @param keyCode the key code of the pressed key.
* @param scanCode the scan code of the pressed key.
* @param modifiers the keyboard modifiers.
*/
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
InputConstants.Key mouseKey = InputConstants.getKey(keyCode, scanCode);
if (super.keyPressed(keyCode, scanCode, modifiers)) {
return true;
} else if (this.minecraft.options.keyInventory.isActiveAndMatches(mouseKey)) {
this.onClose();
return true;
} else {
boolean handled = this.checkHotbarKeyPressed(keyCode, scanCode);// Forge MC-146650: Needs to return true when the key is handled
if (this.hoveredSlot != null && this.hoveredSlot.hasItem()) {
if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 0, ClickType.CLONE);
handled = true;
} else if (this.minecraft.options.keyDrop.isActiveAndMatches(mouseKey)) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, hasControlDown() ? 1 : 0, ClickType.THROW);
handled = true;
}
} else if (this.minecraft.options.keyDrop.isActiveAndMatches(mouseKey)) {
handled = true; // Forge MC-146650: Emulate MC bug, so we don't drop from hotbar when pressing drop without hovering over a item.
}
return handled;
}
}
protected boolean checkHotbarKeyPressed(int keyCode, int scanCode) {
if (this.menu.getCarried().isEmpty() && this.hoveredSlot != null) {
if (this.minecraft.options.keySwapOffhand.isActiveAndMatches(InputConstants.getKey(keyCode, scanCode))) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 40, ClickType.SWAP);
return true;
}
for (int i = 0; i < 9; i++) {
if (this.minecraft.options.keyHotbarSlots[i].isActiveAndMatches(InputConstants.getKey(keyCode, scanCode))) {
this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, i, ClickType.SWAP);
return true;
}
}
}
return false;
}
@Override
public void removed() {
if (this.minecraft.player != null) {
this.menu.removed(this.minecraft.player);
}
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public final void tick() {
super.tick();
if (this.minecraft.player.isAlive() && !this.minecraft.player.isRemoved()) {
this.containerTick();
} else {
this.minecraft.player.closeContainer();
}
}
protected void containerTick() {
}
@Override
public T getMenu() {
return this.menu;
}
@org.jetbrains.annotations.Nullable
public Slot getSlotUnderMouse() { return this.hoveredSlot; }
public int getGuiLeft() { return leftPos; }
public int getGuiTop() { return topPos; }
public int getXSize() { return imageWidth; }
public int getYSize() { return imageHeight; }
protected int slotColor = -2130706433;
public int getSlotColor(int index) {
return slotColor;
}
@Override
public void onClose() {
this.minecraft.player.closeContainer();
super.onClose();
}
}

View File

@@ -0,0 +1,409 @@
package net.minecraft.server.level;
import com.mojang.logging.LogUtils;
import java.util.Objects;
import javax.annotation.Nullable;
import net.minecraft.advancements.CriteriaTriggers;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.item.enchantment.EnchantmentHelper;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.GameMasterBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import org.slf4j.Logger;
public class ServerPlayerGameMode {
private static final Logger LOGGER = LogUtils.getLogger();
protected ServerLevel level;
protected final ServerPlayer player;
private GameType gameModeForPlayer = GameType.DEFAULT_MODE;
@Nullable
private GameType previousGameModeForPlayer;
private boolean isDestroyingBlock;
private int destroyProgressStart;
private BlockPos destroyPos = BlockPos.ZERO;
private int gameTicks;
private boolean hasDelayedDestroy;
private BlockPos delayedDestroyPos = BlockPos.ZERO;
private int delayedTickStart;
private int lastSentState = -1;
public ServerPlayerGameMode(ServerPlayer player) {
this.player = player;
this.level = player.serverLevel();
}
public boolean changeGameModeForPlayer(GameType gameModeForPlayer) {
if (gameModeForPlayer == this.gameModeForPlayer) {
return false;
} else {
this.setGameModeForPlayer(gameModeForPlayer, this.previousGameModeForPlayer);
this.player.onUpdateAbilities();
this.player
.server
.getPlayerList()
.broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE, this.player));
this.level.updateSleepingPlayerList();
if (gameModeForPlayer == GameType.CREATIVE) {
this.player.resetCurrentImpulseContext();
}
return true;
}
}
protected void setGameModeForPlayer(GameType gameModeForPlayer, @Nullable GameType previousGameModeForPlayer) {
this.previousGameModeForPlayer = previousGameModeForPlayer;
this.gameModeForPlayer = gameModeForPlayer;
// Neo: preserve flying state, removed on tick if Attribute or ability no longer applies
boolean wasFlying = this.player.getAbilities().flying;
gameModeForPlayer.updatePlayerAbilities(this.player.getAbilities());
this.player.getAbilities().flying = wasFlying || this.player.getAbilities().flying;
}
public GameType getGameModeForPlayer() {
return this.gameModeForPlayer;
}
@Nullable
public GameType getPreviousGameModeForPlayer() {
return this.previousGameModeForPlayer;
}
public boolean isSurvival() {
return this.gameModeForPlayer.isSurvival();
}
public boolean isCreative() {
return this.gameModeForPlayer.isCreative();
}
public void tick() {
this.gameTicks++;
if (this.hasDelayedDestroy) {
BlockState blockstate = this.level.getBlockState(this.delayedDestroyPos);
if (blockstate.isAir()) {
this.hasDelayedDestroy = false;
} else {
float f = this.incrementDestroyProgress(blockstate, this.delayedDestroyPos, this.delayedTickStart);
if (f >= 1.0F) {
this.hasDelayedDestroy = false;
this.destroyBlock(this.delayedDestroyPos);
}
}
} else if (this.isDestroyingBlock) {
BlockState blockstate1 = this.level.getBlockState(this.destroyPos);
if (blockstate1.isAir()) {
this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
this.lastSentState = -1;
this.isDestroyingBlock = false;
} else {
this.incrementDestroyProgress(blockstate1, this.destroyPos, this.destroyProgressStart);
}
}
}
private float incrementDestroyProgress(BlockState state, BlockPos pos, int startTick) {
int i = this.gameTicks - startTick;
float f = state.getDestroyProgress(this.player, this.player.level(), pos) * (float)(i + 1);
int j = (int)(f * 10.0F);
if (j != this.lastSentState) {
this.level.destroyBlockProgress(this.player.getId(), pos, j);
this.lastSentState = j;
}
return f;
}
private void debugLogging(BlockPos pos, boolean terminate, int sequence, String message) {
}
public void handleBlockBreakAction(BlockPos pos, ServerboundPlayerActionPacket.Action action, Direction face, int maxBuildHeight, int sequence) {
net.neoforged.neoforge.event.entity.player.PlayerInteractEvent.LeftClickBlock event = net.neoforged.neoforge.common.CommonHooks.onLeftClickBlock(player, pos, face, action);
if (event.isCanceled()) {
return;
}
if (!this.player.canInteractWithBlock(pos, 1.0)) {
this.debugLogging(pos, false, sequence, "too far");
} else if (pos.getY() >= maxBuildHeight) {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, "too high");
} else {
if (action == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK) {
if (!this.level.mayInteract(this.player, pos)) {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, "may not interact");
return;
}
if (this.isCreative()) {
this.destroyAndAck(pos, sequence, "creative destroy");
return;
}
if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, "block action restricted");
return;
}
this.destroyProgressStart = this.gameTicks;
float f = 1.0F;
BlockState blockstate = this.level.getBlockState(pos);
if (!blockstate.isAir()) {
EnchantmentHelper.onHitBlock(
this.level,
this.player.getMainHandItem(),
this.player,
this.player,
EquipmentSlot.MAINHAND,
Vec3.atCenterOf(pos),
blockstate,
p_348149_ -> this.player.onEquippedItemBroken(p_348149_, EquipmentSlot.MAINHAND)
);
if (event.getUseBlock() != net.neoforged.neoforge.common.util.TriState.FALSE)
blockstate.attack(this.level, pos, this.player);
f = blockstate.getDestroyProgress(this.player, this.player.level(), pos);
}
if (!blockstate.isAir() && f >= 1.0F) {
this.destroyAndAck(pos, sequence, "insta mine");
} else {
if (this.isDestroyingBlock) {
this.player.connection.send(new ClientboundBlockUpdatePacket(this.destroyPos, this.level.getBlockState(this.destroyPos)));
this.debugLogging(pos, false, sequence, "abort destroying since another started (client insta mine, server disagreed)");
}
this.isDestroyingBlock = true;
this.destroyPos = pos.immutable();
int i = (int)(f * 10.0F);
this.level.destroyBlockProgress(this.player.getId(), pos, i);
this.debugLogging(pos, true, sequence, "actual start of destroying");
this.lastSentState = i;
}
} else if (action == ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK) {
if (pos.equals(this.destroyPos)) {
int j = this.gameTicks - this.destroyProgressStart;
BlockState blockstate1 = this.level.getBlockState(pos);
if (!blockstate1.isAir()) {
float f1 = blockstate1.getDestroyProgress(this.player, this.player.level(), pos) * (float)(j + 1);
if (f1 >= 0.7F) {
this.isDestroyingBlock = false;
this.level.destroyBlockProgress(this.player.getId(), pos, -1);
this.destroyAndAck(pos, sequence, "destroyed");
return;
}
if (!this.hasDelayedDestroy) {
this.isDestroyingBlock = false;
this.hasDelayedDestroy = true;
this.delayedDestroyPos = pos;
this.delayedTickStart = this.destroyProgressStart;
}
}
}
this.debugLogging(pos, true, sequence, "stopped destroying");
} else if (action == ServerboundPlayerActionPacket.Action.ABORT_DESTROY_BLOCK) {
this.isDestroyingBlock = false;
if (!Objects.equals(this.destroyPos, pos)) {
LOGGER.warn("Mismatch in destroy block pos: {} {}", this.destroyPos, pos);
this.level.destroyBlockProgress(this.player.getId(), this.destroyPos, -1);
this.debugLogging(pos, true, sequence, "aborted mismatched destroying");
}
this.level.destroyBlockProgress(this.player.getId(), pos, -1);
this.debugLogging(pos, true, sequence, "aborted destroying");
}
}
}
public void destroyAndAck(BlockPos pos, int sequence, String message) {
if (this.destroyBlock(pos)) {
this.debugLogging(pos, true, sequence, message);
} else {
this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos)));
this.debugLogging(pos, false, sequence, message);
}
}
/**
* Attempts to harvest a block
*/
public boolean destroyBlock(BlockPos pos) {
BlockState blockstate1 = this.level.getBlockState(pos);
var event = net.neoforged.neoforge.common.CommonHooks.fireBlockBreak(level, gameModeForPlayer, player, pos, blockstate1);
if (event.isCanceled()) {
return false;
} else {
BlockEntity blockentity = this.level.getBlockEntity(pos);
Block block = blockstate1.getBlock();
if (block instanceof GameMasterBlock && !this.player.canUseGameMasterBlocks()) {
this.level.sendBlockUpdated(pos, blockstate1, blockstate1, 3);
return false;
} else if (this.player.blockActionRestricted(this.level, pos, this.gameModeForPlayer)) {
return false;
} else {
BlockState blockstate = block.playerWillDestroy(this.level, pos, blockstate1, this.player);
if (this.isCreative()) {
removeBlock(pos, blockstate, false);
return true;
} else {
ItemStack itemstack = this.player.getMainHandItem();
ItemStack itemstack1 = itemstack.copy();
boolean flag1 = blockstate.canHarvestBlock(this.level, pos, this.player); // previously player.hasCorrectToolForDrops(blockstate)
itemstack.mineBlock(this.level, blockstate, pos, this.player);
boolean flag = removeBlock(pos, blockstate, flag1);
if (flag1 && flag) {
block.playerDestroy(this.level, this.player, pos, blockstate, blockentity, itemstack1);
}
// Neo: Fire the PlayerDestroyItemEvent if the tool was broken at any point during the break process
if (itemstack.isEmpty() && !itemstack1.isEmpty()) {
net.neoforged.neoforge.event.EventHooks.onPlayerDestroyItem(this.player, itemstack1, InteractionHand.MAIN_HAND);
}
return true;
}
}
}
}
/**
* Patched-in method that handles actual removal of blocks for {@link #destroyBlock(BlockPos)}.
*
* @param pos The block pos of the destroyed block
* @param state The state of the destroyed block
* @param canHarvest If the player breaking the block can harvest the drops of the block
* @return If the block was removed, as reported by {@link BlockState#onDestroyedByPlayer}.
*/
private boolean removeBlock(BlockPos pos, BlockState state, boolean canHarvest) {
boolean removed = state.onDestroyedByPlayer(this.level, pos, this.player, canHarvest, this.level.getFluidState(pos));
if (removed)
state.getBlock().destroy(this.level, pos, state);
return removed;
}
public InteractionResult useItem(ServerPlayer player, Level level, ItemStack stack, InteractionHand hand) {
if (this.gameModeForPlayer == GameType.SPECTATOR) {
return InteractionResult.PASS;
} else if (player.getCooldowns().isOnCooldown(stack.getItem())) {
return InteractionResult.PASS;
} else {
InteractionResult cancelResult = net.neoforged.neoforge.common.CommonHooks.onItemRightClick(player, hand);
if (cancelResult != null) return cancelResult;
int i = stack.getCount();
int j = stack.getDamageValue();
InteractionResultHolder<ItemStack> interactionresultholder = stack.use(level, player, hand);
ItemStack itemstack = interactionresultholder.getObject();
if (itemstack == stack && itemstack.getCount() == i && itemstack.getUseDuration(player) <= 0 && itemstack.getDamageValue() == j) {
return interactionresultholder.getResult();
} else if (interactionresultholder.getResult() == InteractionResult.FAIL && itemstack.getUseDuration(player) > 0 && !player.isUsingItem()) {
return interactionresultholder.getResult();
} else {
if (stack != itemstack) {
player.setItemInHand(hand, itemstack);
}
if (itemstack.isEmpty()) {
player.setItemInHand(hand, ItemStack.EMPTY);
}
if (!player.isUsingItem()) {
player.inventoryMenu.sendAllDataToRemote();
}
return interactionresultholder.getResult();
}
}
}
public InteractionResult useItemOn(ServerPlayer player, Level level, ItemStack stack, InteractionHand hand, BlockHitResult hitResult) {
BlockPos blockpos = hitResult.getBlockPos();
BlockState blockstate = level.getBlockState(blockpos);
if (!blockstate.getBlock().isEnabled(level.enabledFeatures())) {
return InteractionResult.FAIL;
}
net.neoforged.neoforge.event.entity.player.PlayerInteractEvent.RightClickBlock event = net.neoforged.neoforge.common.CommonHooks.onRightClickBlock(player, hand, blockpos, hitResult);
if (event.isCanceled()) return event.getCancellationResult();
if (this.gameModeForPlayer == GameType.SPECTATOR) {
MenuProvider menuprovider = blockstate.getMenuProvider(level, blockpos);
if (menuprovider != null) {
player.openMenu(menuprovider);
return InteractionResult.SUCCESS;
} else {
return InteractionResult.PASS;
}
} else {
UseOnContext useoncontext = new UseOnContext(player, hand, hitResult);
if (event.getUseItem() != net.neoforged.neoforge.common.util.TriState.FALSE) {
InteractionResult result = stack.onItemUseFirst(useoncontext);
if (result != InteractionResult.PASS) return result;
}
boolean flag = !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty();
boolean flag1 = (player.isSecondaryUseActive() && flag) && !(player.getMainHandItem().doesSneakBypassUse(level, blockpos, player) && player.getOffhandItem().doesSneakBypassUse(level, blockpos, player));
ItemStack itemstack = stack.copy();
if (event.getUseBlock().isTrue() || (event.getUseBlock().isDefault() && !flag1)) {
ItemInteractionResult iteminteractionresult = blockstate.useItemOn(player.getItemInHand(hand), level, player, hand, hitResult);
if (iteminteractionresult.consumesAction()) {
CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(player, blockpos, itemstack);
return iteminteractionresult.result();
}
if (iteminteractionresult == ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION && hand == InteractionHand.MAIN_HAND) {
InteractionResult interactionresult = blockstate.useWithoutItem(level, player, hitResult);
if (interactionresult.consumesAction()) {
CriteriaTriggers.DEFAULT_BLOCK_USE.trigger(player, blockpos);
return interactionresult;
}
}
}
if (event.getUseItem().isTrue() || (!stack.isEmpty() && !player.getCooldowns().isOnCooldown(stack.getItem()))) {
if (event.getUseItem().isFalse()) return InteractionResult.PASS;
InteractionResult interactionresult1;
if (this.isCreative()) {
int i = stack.getCount();
interactionresult1 = stack.useOn(useoncontext);
stack.setCount(i);
} else {
interactionresult1 = stack.useOn(useoncontext);
}
if (interactionresult1.consumesAction()) {
CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(player, blockpos, itemstack);
}
return interactionresult1;
} else {
return InteractionResult.PASS;
}
}
}
/**
* Sets the world instance.
*/
public void setLevel(ServerLevel serverLevel) {
this.level = serverLevel;
}
}

File diff suppressed because it is too large Load Diff

BIN
project_picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

BIN
project_picture_full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

397
scripts/publish_release.py Normal file
View File

@@ -0,0 +1,397 @@
#!/usr/bin/env python3
import glob
import json
import os
import pathlib
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
ROOT = pathlib.Path(__file__).resolve().parent.parent
GRADLE_PROPERTIES = ROOT / "gradle.properties"
def fail(message: str) -> None:
print(f"ERROR: {message}", file=sys.stderr)
raise SystemExit(1)
def read_gradle_properties() -> dict[str, str]:
values: dict[str, str] = {}
for line in GRADLE_PROPERTIES.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, value = stripped.split("=", 1)
values[key.strip()] = value.strip()
return values
def load_release_event() -> dict:
event_path = os.environ.get("GITHUB_EVENT_PATH")
if not event_path:
fail("GITHUB_EVENT_PATH is not set. This script must run inside a release workflow.")
with open(event_path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
if "release" not in payload:
fail("Workflow event payload does not contain release data.")
return payload
def env_required(name: str) -> str:
value = os.environ.get(name, "").strip()
if not value:
fail(f"Missing required environment value: {name}")
return value
def env_optional(name: str, default: str = "") -> str:
return os.environ.get(name, default).strip()
def first_non_empty(*values: str) -> str:
for value in values:
if value:
return value
return ""
def split_csv(value: str) -> list[str]:
return [part.strip() for part in value.split(",") if part.strip()]
def derive_release_channel(tag_name: str, release_name: str, prerelease: bool) -> str:
text = " ".join([tag_name, release_name]).lower()
if "alpha" in text:
return "alpha"
if prerelease or "beta" in text or "rc" in text or "pre" in text:
return "beta"
return "release"
def select_artifact(mod_id: str, version_hint: str) -> pathlib.Path:
pattern = env_optional("RELEASE_ARTIFACT_GLOB", "build/libs/*.jar")
candidates = [
pathlib.Path(path)
for path in glob.glob(str(ROOT / pattern))
if pathlib.Path(path).is_file()
]
filtered = [
path
for path in candidates
if not any(
marker in path.name
for marker in ("-sources", "-javadoc", "-api", "-dev", "-slim")
)
]
if not filtered:
fail(f"No release jar matched {pattern}")
version_name = version_hint[1:] if version_hint.startswith("v") else version_hint
exact_name = f"{mod_id}-{version_name}.jar"
for path in filtered:
if path.name == exact_name:
return path
filtered.sort(key=lambda path: (path.name.count("-"), len(path.name), path.name))
return filtered[0]
def http_json(method: str, url: str, headers: dict[str, str]) -> tuple[int, object]:
request = urllib.request.Request(url, method=method)
for key, value in headers.items():
request.add_header(key, value)
try:
with urllib.request.urlopen(request) as response:
body = response.read().decode("utf-8")
return response.status, json.loads(body) if body else None
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
return error.code, json.loads(body) if body else None
def curl_request(arguments: list[str], expected_codes: set[int]) -> str:
with tempfile.NamedTemporaryFile(delete=False) as output_file:
output_path = output_file.name
try:
command = [
"curl",
"--silent",
"--show-error",
"--location",
"--output",
output_path,
"--write-out",
"%{http_code}",
*arguments,
]
result = subprocess.run(command, capture_output=True, text=True, check=False)
if result.returncode != 0:
fail(result.stderr.strip() or "curl failed")
status_code = int(result.stdout.strip())
body = pathlib.Path(output_path).read_text(encoding="utf-8", errors="replace")
if status_code not in expected_codes:
fail(f"HTTP {status_code} from remote API:\n{body}")
return body
finally:
pathlib.Path(output_path).unlink(missing_ok=True)
def gitea_headers(token: str) -> dict[str, str]:
return {
"Accept": "application/json",
"Authorization": f"token {token}",
}
def attach_to_gitea_release(
server_url: str,
owner: str,
repo: str,
release_id: int,
artifact: pathlib.Path,
token: str,
) -> None:
asset_name = artifact.name
asset_base = (
f"{server_url.rstrip('/')}/api/v1/repos/"
f"{urllib.parse.quote(owner)}/{urllib.parse.quote(repo)}/releases/{release_id}/assets"
)
headers = gitea_headers(token)
status, payload = http_json("GET", asset_base, headers)
if status == 200 and isinstance(payload, list):
for asset in payload:
if asset.get("name") == asset_name:
delete_url = f"{asset_base}/{asset['id']}"
delete_status, _ = http_json("DELETE", delete_url, headers)
if delete_status not in (204, 404):
fail(f"Failed to delete existing Gitea release asset {asset_name}")
elif status != 404:
fail(f"Could not list Gitea release assets (HTTP {status})")
upload_url = f"{asset_base}?name={urllib.parse.quote(asset_name)}"
print(f"Uploading {asset_name} to Gitea release #{release_id}")
curl_request(
[
"--request",
"POST",
"--header",
f"Authorization: token {token}",
"--header",
"Accept: application/json",
"--form",
f"attachment=@{artifact}",
upload_url,
],
{201},
)
def upload_to_modrinth(
artifact: pathlib.Path,
token: str,
project_id: str,
version_number: str,
version_name: str,
changelog: str,
release_channel: str,
game_versions: list[str],
loaders: list[str],
) -> None:
payload = {
"project_id": project_id,
"version_number": version_number,
"version_title": version_name,
"version_type": release_channel,
"status": "listed",
"featured": False,
"loaders": loaders,
"game_versions": game_versions,
"changelog": changelog,
"file_parts": ["file"],
"primary_file": "file",
}
print(f"Publishing {artifact.name} to Modrinth project {project_id}")
curl_request(
[
"--request",
"POST",
"--header",
f"Authorization: {token}",
"--header",
"User-Agent: bagtabs-release-pipeline/1.0 (self-hosted Gitea)",
"--form",
f"data={json.dumps(payload)}",
"--form",
f"file=@{artifact}",
"https://api.modrinth.com/v2/version",
],
{200, 201},
)
def resolve_curseforge_game_version_ids(
token: str,
game_version_names: list[str],
explicit_ids: list[str],
api_base: str,
) -> list[int]:
if explicit_ids:
return [int(value) for value in explicit_ids]
if not game_version_names:
fail("Set CURSEFORGE_GAME_VERSION_IDS or CURSEFORGE_GAME_VERSION_NAMES for CurseForge publishing.")
url = f"{api_base.rstrip('/')}/api/game/versions"
request = urllib.request.Request(url, method="GET")
request.add_header("X-Api-Token", token)
try:
with urllib.request.urlopen(request) as response:
versions = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
fail(f"Could not read CurseForge game versions (HTTP {error.code}):\n{body}")
matches: list[int] = []
missing: list[str] = []
for name in game_version_names:
matched = next((entry for entry in versions if entry.get("name") == name), None)
if not matched:
missing.append(name)
else:
matches.append(int(matched["id"]))
if missing:
fail(
"Could not resolve CurseForge game version names: "
+ ", ".join(missing)
+ ". Set CURSEFORGE_GAME_VERSION_IDS to bypass lookup."
)
return matches
def upload_to_curseforge(
artifact: pathlib.Path,
token: str,
project_id: str,
display_name: str,
changelog: str,
release_channel: str,
game_version_ids: list[int],
api_base: str,
) -> None:
metadata = {
"changelog": changelog,
"changelogType": "markdown",
"displayName": display_name,
"gameVersions": game_version_ids,
"releaseType": release_channel,
}
url = f"{api_base.rstrip('/')}/api/projects/{project_id}/upload-file"
print(f"Publishing {artifact.name} to CurseForge project {project_id}")
curl_request(
[
"--request",
"POST",
"--header",
f"X-Api-Token: {token}",
"--form",
f"metadata={json.dumps(metadata)}",
"--form",
f"file=@{artifact}",
url,
],
{200, 201},
)
def main() -> None:
gradle_properties = read_gradle_properties()
event = load_release_event()
release = event["release"]
repository = event.get("repository", {})
repository_full_name = repository.get("full_name", "")
repository_parts = repository_full_name.split("/", 1) if "/" in repository_full_name else ["", ""]
mod_id = gradle_properties["mod_id"]
mod_name = gradle_properties["mod_name"]
tag_name = release.get("tag_name")
if not tag_name:
fail("Release tag name missing from event payload.")
version_number = tag_name[1:] if tag_name.startswith("v") else tag_name
version_name = release.get("name") or f"{mod_name} {version_number}"
changelog = release.get("body") or f"Release {version_name}"
release_channel = derive_release_channel(tag_name, version_name, bool(release.get("prerelease")))
artifact = select_artifact(mod_id, tag_name)
print(f"Selected artifact: {artifact}")
server_url = first_non_empty(env_optional("GITEA_SERVER_URL"), env_optional("GITHUB_SERVER_URL"))
if not server_url:
fail("Could not determine the Gitea server URL from workflow environment.")
owner = first_non_empty(
repository.get("owner", {}).get("login")
or repository.get("owner_name"),
repository_parts[0],
env_optional("GITEA_REPOSITORY_OWNER"),
)
repo = first_non_empty(
repository.get("name"),
repository_parts[1],
env_optional("GITEA_REPOSITORY_NAME"),
)
if not owner or not repo:
fail("Could not determine repository owner/name from the release event.")
attach_to_gitea_release(
server_url=server_url,
owner=owner,
repo=repo,
release_id=int(release["id"]),
artifact=artifact,
token=env_required("BAGTABS_GITEA_TOKEN"),
)
game_versions = split_csv(env_required("MINECRAFT_VERSIONS"))
loaders = split_csv(env_required("MOD_LOADERS"))
upload_to_modrinth(
artifact=artifact,
token=env_required("MODRINTH_TOKEN"),
project_id=env_required("MODRINTH_PROJECT_ID"),
version_number=version_number,
version_name=version_name,
changelog=changelog,
release_channel=release_channel,
game_versions=game_versions,
loaders=loaders,
)
curseforge_token = env_required("CURSEFORGE_TOKEN")
curseforge_api_base = env_optional("CURSEFORGE_API_BASE", "https://minecraft.curseforge.com")
curseforge_game_version_ids = resolve_curseforge_game_version_ids(
token=curseforge_token,
game_version_names=split_csv(env_optional("CURSEFORGE_GAME_VERSION_NAMES")),
explicit_ids=split_csv(env_optional("CURSEFORGE_GAME_VERSION_IDS")),
api_base=curseforge_api_base,
)
upload_to_curseforge(
artifact=artifact,
token=curseforge_token,
project_id=env_required("CURSEFORGE_PROJECT_ID"),
display_name=version_name,
changelog=changelog,
release_channel=release_channel,
game_version_ids=curseforge_game_version_ids,
api_base=curseforge_api_base,
)
print("Release publishing complete.")
if __name__ == "__main__":
main()

View File

@@ -3,25 +3,44 @@ package com.trunksbomb.bagtabs;
import com.trunksbomb.bagtabs.client.BagScreen; import com.trunksbomb.bagtabs.client.BagScreen;
import com.trunksbomb.bagtabs.client.BagNamerScreen; import com.trunksbomb.bagtabs.client.BagNamerScreen;
import com.trunksbomb.bagtabs.client.BagTabOverlay; import com.trunksbomb.bagtabs.client.BagTabOverlay;
import com.mojang.blaze3d.platform.InputConstants;
import net.minecraft.client.KeyMapping;
import net.neoforged.bus.api.IEventBus; import net.neoforged.bus.api.IEventBus;
import net.minecraft.client.color.item.ItemColor; import net.minecraft.client.color.item.ItemColor;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.DyedItemColor; import net.minecraft.world.item.component.DyedItemColor;
import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.Dist;
import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.client.event.ClientTickEvent;
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.client.event.ScreenEvent;
import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.NeoForge;
import org.lwjgl.glfw.GLFW;
@Mod(value = BagTabs.MODID, dist = Dist.CLIENT) @Mod(value = BagTabs.MODID, dist = Dist.CLIENT)
public class BagTabsClient { public class BagTabsClient {
private static final String KEY_CATEGORY = "key.categories.bagtabs";
private static final KeyMapping OPEN_LAST_BAG = new KeyMapping(
"key.bagtabs.open_last_bag",
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_BACKSLASH,
KEY_CATEGORY
);
public BagTabsClient(IEventBus modEventBus) { public BagTabsClient(IEventBus modEventBus) {
modEventBus.addListener(BagTabsClient::registerScreens); modEventBus.addListener(BagTabsClient::registerScreens);
modEventBus.addListener(BagTabsClient::registerItemColors); modEventBus.addListener(BagTabsClient::registerItemColors);
modEventBus.addListener(BagTabsClient::registerKeyMappings);
NeoForge.EVENT_BUS.addListener(BagTabsClient::renderTabsPre);
NeoForge.EVENT_BUS.addListener(BagTabsClient::renderTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::renderTabs);
NeoForge.EVENT_BUS.addListener(BagTabsClient::clickTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::clickTabs);
NeoForge.EVENT_BUS.addListener(BagTabsClient::dragTabs);
NeoForge.EVENT_BUS.addListener(BagTabsClient::releaseTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::releaseTabs);
NeoForge.EVENT_BUS.addListener(BagTabsClient::scrollTabs);
NeoForge.EVENT_BUS.addListener(BagTabsClient::initScreens);
NeoForge.EVENT_BUS.addListener(BagTabsClient::clientTick);
} }
private static void registerScreens(RegisterMenuScreensEvent event) { private static void registerScreens(RegisterMenuScreensEvent event) {
@@ -34,6 +53,14 @@ public class BagTabsClient {
event.register(bagColor, BagTabs.BAG.get()); event.register(bagColor, BagTabs.BAG.get());
} }
private static void registerKeyMappings(RegisterKeyMappingsEvent event) {
event.register(OPEN_LAST_BAG);
}
private static void renderTabsPre(ScreenEvent.Render.Pre event) {
BagTabOverlay.renderPre(event);
}
private static void renderTabs(ScreenEvent.Render.Post event) { private static void renderTabs(ScreenEvent.Render.Post event) {
BagTabOverlay.render(event); BagTabOverlay.render(event);
} }
@@ -46,6 +73,24 @@ public class BagTabsClient {
BagTabOverlay.mouseReleased(event); BagTabOverlay.mouseReleased(event);
} }
private static void scrollTabs(ScreenEvent.MouseScrolled.Pre event) {
BagTabOverlay.mouseScrolled(event);
}
private static void dragTabs(ScreenEvent.MouseDragged.Pre event) {
BagTabOverlay.mouseDragged(event);
}
private static void initScreens(ScreenEvent.Init.Post event) {
BagTabOverlay.screenInit(event);
}
private static void clientTick(ClientTickEvent.Post event) {
while (OPEN_LAST_BAG.consumeClick()) {
BagTabOverlay.openLastBag();
}
}
private static final class BagItemColor { private static final class BagItemColor {
private static final int DEFAULT_TINT = 0x9E7B4F; private static final int DEFAULT_TINT = 0x9E7B4F;

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 com.trunksbomb.bagtabs.item.BagItem;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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.Inventory;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -21,11 +24,48 @@ public final class BagAccess {
BagIdentityData.ensureBagId(stack); BagIdentityData.ensureBagId(stack);
} }
BagCompat.BagHandler handler = BagCompat.findHandler(stack); BagCompat.BagHandler handler = BagCompat.findHandler(stack);
if (handler != null) { if (handler != null || BagCompat.canShowInTabs(stack)) {
bags.add(new BagEntry(slot, stack.copy(), handler)); 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; 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,9 +1,13 @@
package com.trunksbomb.bagtabs.bag; 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 com.trunksbomb.bagtabs.menu.BagMenu;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import net.minecraft.core.NonNullList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
@@ -14,6 +18,7 @@ import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.core.component.DataComponents; import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.item.component.ItemContainerContents;
public final class BagCompat { public final class BagCompat {
private static final List<BagHandler> HANDLERS = List.of( private static final List<BagHandler> HANDLERS = List.of(
@@ -37,17 +42,27 @@ public final class BagCompat {
} }
} }
for (BagTabsProvider provider : BagTabsApi.getProviders()) {
if (provider.supports(stack)) {
return new ApiBagHandler(provider);
}
}
return null; return null;
} }
public static boolean supportsMenu(AbstractContainerMenu menu) { public static boolean supportsMenu(AbstractContainerMenu menu, net.minecraft.world.entity.player.Player player) {
return getActiveBagSlot(menu) >= 0; return getActiveBagSlot(menu, player) >= 0;
} }
public static boolean canName(ItemStack stack) { public static boolean canName(ItemStack stack) {
return !stack.isEmpty() && (findHandler(stack) != null || looksLikeInventoryItem(stack)); return !stack.isEmpty() && (findHandler(stack) != null || looksLikeInventoryItem(stack));
} }
public static boolean canShowInTabs(ItemStack stack) {
return canName(stack);
}
public static BagIdentity getIdentity(ItemStack stack) { public static BagIdentity getIdentity(ItemStack stack) {
if (!canName(stack)) { if (!canName(stack)) {
return null; return null;
@@ -57,6 +72,14 @@ public final class BagCompat {
return new BagIdentity("bagtabs:uuid:" + BagIdentityData.ensureBagId(stack), "uuid", true); 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); Integer dankFrequency = getDankFrequency(stack);
if (dankFrequency != null && dankFrequency >= 0) { if (dankFrequency != null && dankFrequency >= 0) {
return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true); return new BagIdentity("dankstorage:frequency:" + dankFrequency, "frequency", true);
@@ -68,7 +91,7 @@ public final class BagCompat {
return new BagIdentity(namespace + ":" + normalizedName, "name_mod", false); 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) { if (menu instanceof BagMenu bagMenu) {
return bagMenu.getBagSlot(); return bagMenu.getBagSlot();
} }
@@ -80,25 +103,91 @@ 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; return -1;
} }
public static float getFullness(ItemStack stack) {
if (stack.isEmpty()) {
return -1.0F;
}
if (stack.has(DataComponents.CONTAINER)) {
return getContainerFullness(stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY));
}
BagHandler handler = findHandler(stack);
if (handler == null) {
return -1.0F;
}
float fullness = handler.getFullness(stack);
return fullness < 0.0F ? -1.0F : Math.min(1.0F, fullness);
}
public interface BagHandler { public interface BagHandler {
boolean supports(ItemStack stack); 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; 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; return false;
} }
default int getActiveBagSlot(AbstractContainerMenu menu) { default int getActiveBagSlot(AbstractContainerMenu menu) {
return -1; 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 { private static final class NativeBagHandler implements BagHandler {
@@ -108,14 +197,16 @@ public final class BagCompat {
} }
@Override @Override
public void open(ServerPlayer player, int slot, ItemStack stack) { public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
if (stack.getItem() instanceof InventoryBag inventoryBag) { 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); inventoryBag.openFromInventory(player, slot);
} }
} }
@Override @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) { if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
return false; return false;
} }
@@ -124,11 +215,11 @@ public final class BagCompat {
return bagMenu.insertCarriedStack(carriedStack); return bagMenu.insertCarriedStack(carriedStack);
} }
return BagContainer.insertInto(new BagContainer(player, slot), carriedStack); return BagContainer.insertInto(new BagContainer(player, location), carriedStack);
} }
@Override @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) { if (carriedStack.isEmpty() || carriedStack.getItem() instanceof InventoryBag) {
return false; return false;
} }
@@ -137,7 +228,12 @@ public final class BagCompat {
return bagMenu.canInsertCarriedStack(carriedStack); return bagMenu.canInsertCarriedStack(carriedStack);
} }
return BagContainer.canInsertInto(new BagContainer(player, slot), carriedStack); return BagContainer.canInsertInto(new BagContainer(player, location), carriedStack);
}
@Override
public float getFullness(ItemStack stack) {
return getContainerFullness(stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY));
} }
} }
@@ -166,7 +262,7 @@ public final class BagCompat {
} }
@Override @Override
public void open(ServerPlayer player, int slot, ItemStack stack) { public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
invokeStatic( invokeStatic(
CONTAINER_CLASS, CONTAINER_CLASS,
"openBackpack", "openBackpack",
@@ -194,7 +290,7 @@ public final class BagCompat {
} }
@Override @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()) { if (carriedStack.isEmpty()) {
return false; return false;
} }
@@ -204,7 +300,7 @@ public final class BagCompat {
} }
@Override @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()) { if (carriedStack.isEmpty()) {
return false; return false;
} }
@@ -227,6 +323,18 @@ public final class BagCompat {
throw new RuntimeException("Failed to access Traveler's Backpack storage", exception); throw new RuntimeException("Failed to access Traveler's Backpack storage", exception);
} }
} }
@Override
public float getFullness(ItemStack stack) {
try {
Class<?> wrapperClass = Class.forName(WRAPPER_CLASS);
Object wrapper = wrapperClass.getMethod("fromStack", ItemStack.class).invoke(null, stack);
Object storage = wrapper == null ? null : invoke(wrapper, "getStorage");
return storage == null ? -1.0F : getItemHandlerFullness(storage);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException exception) {
return -1.0F;
}
}
} }
private static final class DankStorageHandler implements BagHandler { private static final class DankStorageHandler implements BagHandler {
@@ -239,7 +347,7 @@ public final class BagCompat {
} }
@Override @Override
public void open(ServerPlayer player, int slot, ItemStack stack) { public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
try { try {
Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class); Method createProvider = stack.getItem().getClass().getMethod("createProvider", ItemStack.class);
Object provider = createProvider.invoke(stack.getItem(), stack); Object provider = createProvider.invoke(stack.getItem(), stack);
@@ -269,7 +377,7 @@ public final class BagCompat {
} }
@Override @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)) { if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
return false; return false;
} }
@@ -279,7 +387,7 @@ public final class BagCompat {
} }
@Override @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)) { if (carriedStack.isEmpty() || ItemStack.isSameItemSameComponents(carriedStack, bagStack)) {
return false; return false;
} }
@@ -346,6 +454,11 @@ public final class BagCompat {
stack.shrink(inserted); stack.shrink(inserted);
return true; return true;
} }
@Override
public float getFullness(ItemStack stack) {
return -1.0F;
}
} }
private static final class SophisticatedBackpacksHandler implements BagHandler { private static final class SophisticatedBackpacksHandler implements BagHandler {
@@ -360,7 +473,7 @@ public final class BagCompat {
} }
@Override @Override
public void open(ServerPlayer player, int slot, ItemStack stack) { public void open(ServerPlayer player, int slot, BagLocation location, ItemStack stack) {
try { try {
Class<?> payloadClass = Class.forName(PAYLOAD_CLASS); Class<?> payloadClass = Class.forName(PAYLOAD_CLASS);
Method handlePayload = payloadClass.getMethod( Method handlePayload = payloadClass.getMethod(
@@ -391,7 +504,7 @@ public final class BagCompat {
} }
@Override @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()) { if (carriedStack.isEmpty()) {
return false; return false;
} }
@@ -401,7 +514,7 @@ public final class BagCompat {
} }
@Override @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()) { if (carriedStack.isEmpty()) {
return false; return false;
} }
@@ -425,6 +538,18 @@ public final class BagCompat {
return wrapper == null ? null : invoke(wrapper, "getInventoryHandler"); return wrapper == null ? null : invoke(wrapper, "getInventoryHandler");
} }
@Override
public float getFullness(ItemStack stack) {
try {
Class<?> wrapperClass = Class.forName("net.p3pp3rf1y.sophisticatedbackpacks.backpack.wrapper.BackpackWrapper");
Object wrapper = wrapperClass.getMethod("fromStack", ItemStack.class).invoke(null, stack);
Object inventoryHandler = wrapper == null ? null : invoke(wrapper, "getInventoryHandler");
return inventoryHandler == null ? -1.0F : getItemHandlerFullness(inventoryHandler);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException exception) {
return -1.0F;
}
}
} }
private static int findMatchingPlayerSlot(AbstractContainerMenu menu, ItemStack bagStack) { private static int findMatchingPlayerSlot(AbstractContainerMenu menu, ItemStack bagStack) {
@@ -612,4 +737,35 @@ public final class BagCompat {
throw new RuntimeException("Failed to invoke compatibility method " + className + "#" + methodName, exception); throw new RuntimeException("Failed to invoke compatibility method " + className + "#" + methodName, exception);
} }
} }
private static float getContainerFullness(ItemContainerContents contents) {
NonNullList<ItemStack> items = NonNullList.withSize(27, ItemStack.EMPTY);
contents.copyInto(items);
return calculateFullness(items);
}
private static float getItemHandlerFullness(Object itemHandler) {
int slots = (Integer) invoke(itemHandler, "getSlots");
List<ItemStack> items = new java.util.ArrayList<>(slots);
for (int slot = 0; slot < slots; slot++) {
items.add((ItemStack) invoke(itemHandler, "getStackInSlot", new Class<?>[] {int.class}, slot));
}
return calculateFullness(items);
}
private static float calculateFullness(List<ItemStack> items) {
if (items.isEmpty()) {
return -1.0F;
}
float usedCapacity = 0.0F;
for (ItemStack item : items) {
if (item.isEmpty()) {
continue;
}
usedCapacity += item.getCount() / (float) Math.max(1, item.getMaxStackSize());
}
return Math.min(1.0F, usedCapacity / items.size());
}
} }

View File

@@ -10,14 +10,14 @@ import net.minecraft.world.item.component.ItemContainerContents;
public class BagContainer extends SimpleContainer { public class BagContainer extends SimpleContainer {
private final Player player; 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); super(BagItem.SLOT_COUNT);
this.player = player; 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()); contents.copyInto(this.getItems());
} }
@@ -29,12 +29,14 @@ public class BagContainer extends SimpleContainer {
@Override @Override
public void setChanged() { public void setChanged() {
super.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) { if (!bagStack.isEmpty() && bagStack.getItem() instanceof BagItem) {
bagStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.copyItems())); bagStack.set(DataComponents.CONTAINER, ItemContainerContents.fromItems(this.copyItems()));
if (this.location.setStack(this.player, bagStack)) {
this.player.getInventory().setChanged(); this.player.getInventory().setChanged();
} }
} }
}
@Override @Override
public boolean stillValid(Player player) { public boolean stillValid(Player player) {
@@ -42,7 +44,7 @@ public class BagContainer extends SimpleContainer {
return false; return false;
} }
ItemStack current = player.getInventory().getItem(this.slot); ItemStack current = this.location.getStack(player);
return current.getItem() instanceof BagItem; return current.getItem() instanceof BagItem;
} }

View File

@@ -2,5 +2,5 @@ package com.trunksbomb.bagtabs.bag;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler) { 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,291 @@
package com.trunksbomb.bagtabs.client;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.trunksbomb.bagtabs.BagTabs;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ServerData;
public final class DockConfigManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static SessionSettings cachedSettings = new SessionSettings();
private static Path cachedPath;
private DockConfigManager() {
}
public static DockSettings getEffectiveSettings(String screenKey) {
SessionSettings settings = getSessionSettings();
DockSettings override = settings.screenOverrides.get(screenKey);
return override == null ? settings.globalDefault : override;
}
public static DockSettings getEditableSettings(String screenKey, boolean editOverride) {
SessionSettings settings = getSessionSettings();
if (!editOverride) {
return settings.globalDefault;
}
return settings.screenOverrides.getOrDefault(screenKey, settings.globalDefault.copy());
}
public static boolean hasOverride(String screenKey) {
return getSessionSettings().screenOverrides.containsKey(screenKey);
}
public static void setEditableSettings(String screenKey, boolean editOverride, DockSettings dockSettings) {
SessionSettings settings = getSessionSettings();
if (editOverride) {
settings.screenOverrides.put(screenKey, dockSettings.copy());
} else {
settings.globalDefault = dockSettings.copy();
}
save();
}
public static void setEffectiveOffsets(String screenKey, int xOffset, int yOffset) {
SessionSettings settings = getSessionSettings();
if (settings.screenOverrides.containsKey(screenKey)) {
DockSettings override = settings.screenOverrides.get(screenKey);
settings.screenOverrides.put(screenKey, override.withXOffset(xOffset).withYOffset(yOffset));
} else {
settings.globalDefault = settings.globalDefault.withXOffset(xOffset).withYOffset(yOffset);
}
save();
}
public static void clearOverride(String screenKey) {
SessionSettings settings = getSessionSettings();
settings.screenOverrides.remove(screenKey);
save();
}
public static boolean isInteractionsLocked() {
return getSessionSettings().interactionsLocked;
}
public static void setInteractionsLocked(boolean locked) {
SessionSettings settings = getSessionSettings();
settings.interactionsLocked = locked;
save();
}
public static void toggleInteractionsLocked() {
setInteractionsLocked(!isInteractionsLocked());
}
public static int getRememberedPage(String screenKey) {
return getSessionSettings().rememberedPages.getOrDefault(screenKey, 0);
}
public static void setRememberedPage(String screenKey, int page) {
SessionSettings settings = getSessionSettings();
settings.rememberedPages.put(screenKey, Math.max(0, page));
save();
}
private static SessionSettings getSessionSettings() {
Path path = getConfigPath();
if (!Objects.equals(path, cachedPath)) {
cachedPath = path;
cachedSettings = load(path);
}
return cachedSettings;
}
private static SessionSettings load(Path path) {
if (!Files.exists(path)) {
return new SessionSettings();
}
try (Reader reader = Files.newBufferedReader(path)) {
SessionSettings settings = GSON.fromJson(reader, SessionSettings.class);
return settings == null ? new SessionSettings() : settings.normalize();
} catch (IOException exception) {
BagTabs.LOGGER.warn("Failed to load dock config", exception);
return new SessionSettings();
}
}
private static void save() {
if (cachedPath == null) {
cachedPath = getConfigPath();
}
try {
Files.createDirectories(cachedPath.getParent());
try (Writer writer = Files.newBufferedWriter(cachedPath)) {
GSON.toJson(cachedSettings, writer);
}
} catch (IOException exception) {
BagTabs.LOGGER.warn("Failed to save dock config", exception);
}
}
private static Path getConfigPath() {
Minecraft minecraft = Minecraft.getInstance();
String playerKey = minecraft.player != null ? minecraft.player.getUUID().toString() : "unknown-player";
String worldKey = "default";
if (minecraft.hasSingleplayerServer() && minecraft.getSingleplayerServer() != null) {
worldKey = "singleplayer-" + sanitize(minecraft.getSingleplayerServer().getWorldData().getLevelName());
} else {
ServerData currentServer = minecraft.getCurrentServer();
if (currentServer != null) {
worldKey = "server-" + sanitize(currentServer.ip);
}
}
return minecraft.gameDirectory.toPath()
.resolve("config")
.resolve("bagtabs")
.resolve(playerKey)
.resolve(worldKey + ".json");
}
private static String sanitize(String value) {
return value == null ? "unknown" : value.replaceAll("[^a-zA-Z0-9._-]", "_");
}
public enum DockSide {
BOTTOM,
TOP,
LEFT,
RIGHT,
SCREEN_BOTTOM,
SCREEN_TOP,
SCREEN_LEFT,
SCREEN_RIGHT,
FLOATING_HORIZONTAL,
FLOATING_VERTICAL;
public DockSide next() {
DockSide[] values = values();
return values[(ordinal() + 1) % values.length];
}
public DockSide previous() {
DockSide[] values = values();
return values[(ordinal() - 1 + values.length) % values.length];
}
}
public static final class DockSettings {
private DockSide dockSide = DockSide.BOTTOM;
private int xOffset = 0;
private int yOffset = 0;
private int maxTabs = 8;
private boolean compact = false;
private boolean showFullnessIndicator = false;
public DockSide dockSide() {
return dockSide;
}
public int xOffset() {
return xOffset;
}
public int yOffset() {
return yOffset;
}
public int maxTabs() {
return maxTabs;
}
public boolean compact() {
return compact;
}
public boolean showFullnessIndicator() {
return showFullnessIndicator;
}
public DockSettings withDockSide(DockSide nextDockSide) {
DockSettings copy = copy();
copy.dockSide = nextDockSide;
return copy;
}
public DockSettings withXOffset(int nextXOffset) {
DockSettings copy = copy();
copy.xOffset = nextXOffset;
return copy;
}
public DockSettings withYOffset(int nextYOffset) {
DockSettings copy = copy();
copy.yOffset = nextYOffset;
return copy;
}
public DockSettings withMaxTabs(int nextMaxTabs) {
DockSettings copy = copy();
copy.maxTabs = Math.max(1, Math.min(20, nextMaxTabs));
return copy;
}
public DockSettings withCompact(boolean nextCompact) {
DockSettings copy = copy();
copy.compact = nextCompact;
return copy;
}
public DockSettings withShowFullnessIndicator(boolean nextShowFullnessIndicator) {
DockSettings copy = copy();
copy.showFullnessIndicator = nextShowFullnessIndicator;
return copy;
}
public DockSettings copy() {
DockSettings copy = new DockSettings();
copy.dockSide = this.dockSide;
copy.xOffset = this.xOffset;
copy.yOffset = this.yOffset;
copy.maxTabs = this.maxTabs;
copy.compact = this.compact;
copy.showFullnessIndicator = this.showFullnessIndicator;
return copy;
}
private DockSettings normalize() {
if (dockSide == null) {
dockSide = DockSide.FLOATING_HORIZONTAL;
}
maxTabs = Math.max(1, Math.min(20, maxTabs));
return this;
}
}
private static final class SessionSettings {
private boolean interactionsLocked = false;
private DockSettings globalDefault = new DockSettings();
private Map<String, DockSettings> screenOverrides = new HashMap<>();
private Map<String, Integer> rememberedPages = new HashMap<>();
private SessionSettings normalize() {
if (globalDefault == null) {
globalDefault = new DockSettings();
}
globalDefault = globalDefault.normalize();
if (screenOverrides == null) {
screenOverrides = new HashMap<>();
} else {
screenOverrides.replaceAll((key, value) -> value == null ? new DockSettings() : value.normalize());
}
if (rememberedPages == null) {
rememberedPages = new HashMap<>();
}
return this;
}
}
}

View File

@@ -0,0 +1,242 @@
package com.trunksbomb.bagtabs.client;
import com.trunksbomb.bagtabs.BagTabs;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
public class DockConfigScreen extends Screen {
private static final int PANEL_WIDTH = 196;
private static final int PANEL_HEIGHT = 234;
private static final int TITLE_BAR_HEIGHT = 18;
private final Screen parent;
private final String screenKey;
private boolean editOverride;
private DockConfigManager.DockSettings dockSettings;
private int panelX;
private int panelY;
private boolean draggingPanel;
private int dragOffsetX;
private int dragOffsetY;
private Button targetButton;
private Button sideButton;
private Button xMinusButton;
private Button xPlusButton;
private Button yMinusButton;
private Button yPlusButton;
private Button maxTabsMinusButton;
private Button maxTabsPlusButton;
private Button compactButton;
private Button resetButton;
private Button doneButton;
public DockConfigScreen(Screen parent) {
super(BagTabs.translation("dock.title"));
this.parent = parent;
this.screenKey = parent.getClass().getName();
this.editOverride = DockConfigManager.hasOverride(this.screenKey);
this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, this.editOverride).copy();
}
@Override
protected void init() {
if (this.panelX == 0 && this.panelY == 0) {
this.panelX = (this.width - PANEL_WIDTH) / 2;
this.panelY = (this.height - PANEL_HEIGHT) / 2;
}
this.targetButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> {
saveCurrent();
this.editOverride = !this.editOverride;
this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, this.editOverride).copy();
syncLabels();
}).bounds(0, 0, 180, 20).build());
this.sideButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> {
this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().next());
saveCurrent();
syncLabels();
}).bounds(0, 0, 180, 20).build());
this.xMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustX(-getOffsetStep()))
.bounds(0, 0, 20, 20).build());
this.xPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustX(getOffsetStep()))
.bounds(0, 0, 20, 20).build());
this.yMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustY(-getOffsetStep()))
.bounds(0, 0, 20, 20).build());
this.yPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustY(getOffsetStep()))
.bounds(0, 0, 20, 20).build());
this.maxTabsMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustMaxTabs(-1))
.bounds(0, 0, 20, 20).build());
this.maxTabsPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustMaxTabs(1))
.bounds(0, 0, 20, 20).build());
this.compactButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> {
this.dockSettings = this.dockSettings.withCompact(!this.dockSettings.compact());
saveCurrent();
syncLabels();
}).bounds(0, 0, 180, 20).build());
this.resetButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.reset"), button -> {
DockConfigManager.clearOverride(this.screenKey);
this.editOverride = false;
this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, false).copy();
syncLabels();
}).bounds(0, 0, 180, 20).build());
this.doneButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.done"), button -> onClose())
.bounds(0, 0, 180, 20).build());
syncLabels();
Tooltip offsetTooltip = Tooltip.create(BagTabs.translation("dock.offset_steps"));
this.xMinusButton.setTooltip(offsetTooltip);
this.xPlusButton.setTooltip(offsetTooltip);
this.yMinusButton.setTooltip(offsetTooltip);
this.yPlusButton.setTooltip(offsetTooltip);
updateWidgetPositions();
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
this.parent.render(guiGraphics, mouseX, mouseY, partialTick);
guiGraphics.fill(0, 0, this.width, this.height, 0x5A000000);
guiGraphics.fill(this.panelX, this.panelY, this.panelX + PANEL_WIDTH, this.panelY + PANEL_HEIGHT, 0xE03A3A3A);
guiGraphics.fill(this.panelX + 1, this.panelY + 1, this.panelX + PANEL_WIDTH - 1, this.panelY + PANEL_HEIGHT - 1, 0xE01E1E1E);
guiGraphics.fill(this.panelX + 2, this.panelY + 2, this.panelX + PANEL_WIDTH - 2, this.panelY + TITLE_BAR_HEIGHT, 0xFF5A5A5A);
guiGraphics.fill(this.panelX + 2, this.panelY + TITLE_BAR_HEIGHT, this.panelX + PANEL_WIDTH - 2, this.panelY + TITLE_BAR_HEIGHT + 1, 0xFF000000);
super.render(guiGraphics, mouseX, mouseY, partialTick);
int centerX = this.panelX + (PANEL_WIDTH / 2);
int top = this.panelY + 12;
guiGraphics.drawCenteredString(this.font, this.title, centerX, this.panelY + 6, 0xFFFFFF);
guiGraphics.drawCenteredString(this.font, Component.literal("X Offset: " + this.dockSettings.xOffset()), centerX, top + 62, 0xFFFFFF);
guiGraphics.drawCenteredString(this.font, Component.literal("Y Offset: " + this.dockSettings.yOffset()), centerX, top + 88, 0xFFFFFF);
guiGraphics.drawCenteredString(this.font, Component.literal("Max Tabs: " + this.dockSettings.maxTabs()), centerX, top + 114, 0xFFFFFF);
}
@Override
public void onClose() {
saveCurrent();
Minecraft.getInstance().setScreen(this.parent);
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (button == 0 && isOverTitleBar(mouseX, mouseY)) {
this.draggingPanel = true;
this.dragOffsetX = (int) mouseX - this.panelX;
this.dragOffsetY = (int) mouseY - this.panelY;
return true;
}
if (button == 1 && this.sideButton != null && this.sideButton.isMouseOver(mouseX, mouseY)) {
this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().previous());
saveCurrent();
syncLabels();
return true;
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (button == 0 && this.draggingPanel) {
this.panelX = clamp((int) mouseX - this.dragOffsetX, 4, this.width - PANEL_WIDTH - 4);
this.panelY = clamp((int) mouseY - this.dragOffsetY, 4, this.height - PANEL_HEIGHT - 4);
updateWidgetPositions();
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
}
@Override
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (button == 0) {
this.draggingPanel = false;
}
return super.mouseReleased(mouseX, mouseY, button);
}
private void adjustX(int delta) {
this.dockSettings = this.dockSettings.withXOffset(this.dockSettings.xOffset() + delta);
saveCurrent();
syncLabels();
}
private void adjustY(int delta) {
this.dockSettings = this.dockSettings.withYOffset(this.dockSettings.yOffset() + delta);
saveCurrent();
syncLabels();
}
private void adjustMaxTabs(int delta) {
this.dockSettings = this.dockSettings.withMaxTabs(this.dockSettings.maxTabs() + delta);
saveCurrent();
syncLabels();
}
private int getOffsetStep() {
boolean shift = hasShiftDown();
boolean control = hasControlDown();
if (shift && control) {
return 100;
}
if (control) {
return 25;
}
if (shift) {
return 5;
}
return 1;
}
private void syncLabels() {
this.targetButton.setMessage(this.editOverride
? BagTabs.translation("dock.target.override")
: BagTabs.translation("dock.target.global"));
this.sideButton.setMessage(BagTabs.translation("dock.side." + this.dockSettings.dockSide().name().toLowerCase()));
this.compactButton.setMessage(BagTabs.translation(this.dockSettings.compact() ? "dock.compact.on" : "dock.compact.off"));
this.resetButton.active = this.editOverride || DockConfigManager.hasOverride(this.screenKey);
}
private void saveCurrent() {
DockConfigManager.setEditableSettings(this.screenKey, this.editOverride, this.dockSettings);
}
private void updateWidgetPositions() {
int left = this.panelX + 8;
int top = this.panelY + 26;
this.targetButton.setPosition(left, top);
this.sideButton.setPosition(left, top + 26);
this.xMinusButton.setPosition(left, top + 56);
this.xPlusButton.setPosition(left + 160, top + 56);
this.yMinusButton.setPosition(left, top + 82);
this.yPlusButton.setPosition(left + 160, top + 82);
this.maxTabsMinusButton.setPosition(left, top + 108);
this.maxTabsPlusButton.setPosition(left + 160, top + 108);
this.compactButton.setPosition(left, top + 134);
this.resetButton.setPosition(left, top + 160);
this.doneButton.setPosition(left, top + 186);
}
private boolean isOverTitleBar(double mouseX, double mouseY) {
return mouseX >= this.panelX + 2
&& mouseX < this.panelX + PANEL_WIDTH - 2
&& mouseY >= this.panelY + 2
&& mouseY < this.panelY + TITLE_BAR_HEIGHT;
}
private int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}

View File

@@ -0,0 +1,212 @@
package com.trunksbomb.bagtabs.client;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagEntry;
import com.trunksbomb.bagtabs.bag.BagIdentity;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import net.minecraft.client.Minecraft;
public final class TabPinManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final Type PINNED_TAB_LIST_TYPE = new TypeToken<List<PinnedTab>>() {}.getType();
private static boolean loaded = false;
private static final List<PinnedTab> PINNED_TABS = new ArrayList<>();
private TabPinManager() {
}
public static List<BagEntry> sortTabs(List<BagEntry> bags) {
ensureLoaded();
return bags.stream()
.sorted(Comparator
.comparingInt((BagEntry bag) -> isPinned(bag, bags) ? 0 : 1)
.thenComparingInt(bag -> getPinnedOrder(bag, bags))
.thenComparingInt(BagEntry::slot))
.toList();
}
public static boolean isPinned(BagEntry bag, List<BagEntry> allBags) {
return resolvePinnedBag(bag.identity(), allBags)
.map(resolved -> resolved.slot() == bag.slot())
.orElse(false);
}
public static String getPinFailureReason(BagEntry bag, List<BagEntry> allBags) {
BagIdentity identity = bag.identity();
if (identity == null || identity.stable()) {
return null;
}
for (PinnedTab pinnedTab : PINNED_TABS) {
if (!pinnedTab.identityKey().equals(identity.key())) {
continue;
}
BagEntry resolved = resolvePinnedBag(identity, allBags).orElse(null);
if (resolved != null && resolved.slot() != bag.slot()) {
return "Only one fallback-identified bag with this name can be pinned.";
}
}
return null;
}
public static ToggleResult togglePin(BagEntry bag, List<BagEntry> allBags) {
ensureLoaded();
BagIdentity identity = bag.identity();
if (identity == null) {
return new ToggleResult(false, "This bag can't be pinned yet.");
}
if (isPinned(bag, allBags)) {
PINNED_TABS.removeIf(pinnedTab -> pinnedTab.identityKey().equals(identity.key()));
save();
return new ToggleResult(true, null);
}
String failureReason = getPinFailureReason(bag, allBags);
if (failureReason != null) {
return new ToggleResult(false, failureReason);
}
PINNED_TABS.add(new PinnedTab(identity.key(), identity.stable(), bag.slot()));
save();
return new ToggleResult(true, null);
}
public static List<String> getPinnedIdentityOrder(List<BagEntry> allBags) {
ensureLoaded();
List<String> order = new ArrayList<>();
for (PinnedTab pinnedTab : PINNED_TABS) {
resolvePinnedBag(pinnedTab.identityKey(), allBags).ifPresent(resolved -> order.add(resolved.identity().key()));
}
return order;
}
public static void applyPinnedOrder(List<String> orderedIdentityKeys, List<BagEntry> allBags) {
ensureLoaded();
List<PinnedTab> reordered = new ArrayList<>();
for (String identityKey : orderedIdentityKeys) {
PinnedTab existing = PINNED_TABS.stream()
.filter(tab -> tab.identityKey().equals(identityKey))
.findFirst()
.orElse(null);
BagEntry resolved = resolvePinnedBag(identityKey, allBags).orElse(null);
if (existing != null && resolved != null) {
reordered.add(new PinnedTab(identityKey, existing.stable(), resolved.slot()));
}
}
for (PinnedTab existing : PINNED_TABS) {
if (reordered.stream().noneMatch(tab -> tab.identityKey().equals(existing.identityKey()))) {
reordered.add(existing);
}
}
PINNED_TABS.clear();
PINNED_TABS.addAll(reordered);
save();
}
private static int getPinnedOrder(BagEntry bag, List<BagEntry> allBags) {
for (int i = 0; i < PINNED_TABS.size(); i++) {
BagEntry resolved = resolvePinnedBag(PINNED_TABS.get(i).identityKey(), allBags).orElse(null);
if (resolved != null && resolved.slot() == bag.slot()) {
return i;
}
}
return Integer.MAX_VALUE;
}
private static java.util.Optional<BagEntry> resolvePinnedBag(BagIdentity identity, List<BagEntry> allBags) {
return identity == null ? java.util.Optional.empty() : resolvePinnedBag(identity.key(), allBags);
}
private static java.util.Optional<BagEntry> resolvePinnedBag(String identityKey, List<BagEntry> allBags) {
PinnedTab pinnedTab = PINNED_TABS.stream()
.filter(tab -> tab.identityKey().equals(identityKey))
.findFirst()
.orElse(null);
if (pinnedTab == null) {
return java.util.Optional.empty();
}
List<BagEntry> matches = allBags.stream()
.filter(bag -> bag.identity() != null && Objects.equals(bag.identity().key(), identityKey))
.sorted(Comparator.comparingInt(BagEntry::slot))
.toList();
if (matches.isEmpty()) {
return java.util.Optional.empty();
}
return matches.stream()
.filter(match -> match.slot() == pinnedTab.preferredSlot())
.findFirst()
.or(() -> java.util.Optional.of(matches.getFirst()));
}
private static void ensureLoaded() {
if (loaded) {
return;
}
loaded = true;
Path path = getConfigPath();
if (!Files.exists(path)) {
return;
}
try (Reader reader = Files.newBufferedReader(path)) {
List<PinnedTab> loadedTabs = GSON.fromJson(reader, PINNED_TAB_LIST_TYPE);
if (loadedTabs != null) {
PINNED_TABS.clear();
Set<String> seenKeys = new HashSet<>();
for (PinnedTab pinnedTab : loadedTabs) {
if (seenKeys.add(pinnedTab.identityKey())) {
PINNED_TABS.add(pinnedTab);
}
}
}
} catch (IOException exception) {
BagTabs.LOGGER.warn("Failed to load pinned bag tab state", exception);
}
}
private static void save() {
Path path = getConfigPath();
try {
Files.createDirectories(path.getParent());
try (Writer writer = Files.newBufferedWriter(path)) {
GSON.toJson(PINNED_TABS, PINNED_TAB_LIST_TYPE, writer);
}
} catch (IOException exception) {
BagTabs.LOGGER.warn("Failed to save pinned bag tab state", exception);
}
}
private static Path getConfigPath() {
return Minecraft.getInstance().gameDirectory.toPath().resolve("config").resolve("bagtabs-pinned-tabs.json");
}
private record PinnedTab(String identityKey, boolean stable, int preferredSlot) {
}
public record ToggleResult(boolean changed, String failureReason) {
}
}

View File

@@ -3,6 +3,8 @@ package com.trunksbomb.bagtabs.item;
import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagContainer; import com.trunksbomb.bagtabs.bag.BagContainer;
import com.trunksbomb.bagtabs.bag.BagIdentityData; 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.bag.InventoryBag;
import com.trunksbomb.bagtabs.menu.BagMenu; import com.trunksbomb.bagtabs.menu.BagMenu;
import java.util.ArrayList; import java.util.ArrayList;
@@ -61,7 +63,11 @@ public class BagItem extends Item implements InventoryBag {
@Override @Override
public void openFromInventory(ServerPlayer player, int slot) { 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)) { if (!(stack.getItem() instanceof BagItem)) {
return; return;
} }
@@ -73,7 +79,7 @@ public class BagItem extends Item implements InventoryBag {
new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu( new SimpleMenuProvider((containerId, playerInventory, ignoredPlayer) -> new BagMenu(
containerId, containerId,
playerInventory, playerInventory,
new BagContainer(player, slot), new BagContainer(player, location),
slot slot
), title), ), title),
buf -> buf.writeVarInt(slot) buf -> buf.writeVarInt(slot)

View File

@@ -1,7 +1,10 @@
package com.trunksbomb.bagtabs.menu; package com.trunksbomb.bagtabs.menu;
import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagAccess;
import com.trunksbomb.bagtabs.bag.BagContainer; 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 com.trunksbomb.bagtabs.bag.InventoryBag;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.Container; import net.minecraft.world.Container;
@@ -23,7 +26,13 @@ public class BagMenu extends AbstractContainerMenu {
public static BagMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) { public static BagMenu fromNetwork(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf extraData) {
int slot = extraData.readVarInt(); 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) { public BagMenu(int containerId, Inventory playerInventory, Container container, int bagSlot) {

View File

@@ -1,6 +1,8 @@
package com.trunksbomb.bagtabs.network; package com.trunksbomb.bagtabs.network;
import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagAccess;
import com.trunksbomb.bagtabs.bag.BagEntry;
import com.trunksbomb.bagtabs.bag.BagCompat; import com.trunksbomb.bagtabs.bag.BagCompat;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.ByteBufCodecs;
@@ -28,11 +30,12 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload {
return; return;
} }
if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) { BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot());
if (entry == null) {
return; return;
} }
ItemStack bagStack = serverPlayer.getInventory().getItem(payload.slot()); ItemStack bagStack = entry.location().getStack(serverPlayer);
ItemStack carriedStack = serverPlayer.containerMenu.getCarried(); ItemStack carriedStack = serverPlayer.containerMenu.getCarried();
if (carriedStack.isEmpty() || bagStack.isEmpty()) { if (carriedStack.isEmpty() || bagStack.isEmpty()) {
return; return;
@@ -42,8 +45,8 @@ public record InsertIntoBagPayload(int slot) implements CustomPacketPayload {
return; return;
} }
BagCompat.BagHandler handler = BagCompat.findHandler(bagStack); BagCompat.BagHandler handler = entry.handler();
if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), bagStack, carriedStack)) { if (handler != null && handler.insertFromCarried(serverPlayer, payload.slot(), entry.location(), bagStack, carriedStack)) {
serverPlayer.containerMenu.broadcastChanges(); serverPlayer.containerMenu.broadcastChanges();
} }
} }

View File

@@ -1,14 +1,30 @@
package com.trunksbomb.bagtabs.network; package com.trunksbomb.bagtabs.network;
import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.BagTabs;
import com.trunksbomb.bagtabs.bag.BagAccess;
import com.trunksbomb.bagtabs.bag.BagEntry;
import com.trunksbomb.bagtabs.bag.BagCompat; import com.trunksbomb.bagtabs.bag.BagCompat;
import com.trunksbomb.bagtabs.bag.InventoryBag; import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.network.protocol.game.ClientboundSetCarriedItemPacket;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.BowItem;
import net.minecraft.world.item.CrossbowItem;
import net.minecraft.world.item.DiggerItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ProjectileWeaponItem;
import net.minecraft.world.item.ShearsItem;
import net.minecraft.world.item.ShieldItem;
import net.minecraft.world.item.SwordItem;
import net.minecraft.world.item.TridentItem;
import net.neoforged.neoforge.network.handling.IPayloadContext; import net.neoforged.neoforge.network.handling.IPayloadContext;
public record OpenBagPayload(int slot) implements CustomPacketPayload { public record OpenBagPayload(int slot) implements CustomPacketPayload {
@@ -29,14 +45,106 @@ public record OpenBagPayload(int slot) implements CustomPacketPayload {
return; return;
} }
if (payload.slot() < 0 || payload.slot() >= serverPlayer.getInventory().getContainerSize()) { BagEntry entry = BagAccess.findBag(serverPlayer, payload.slot());
if (entry == null) {
return; return;
} }
ItemStack stack = serverPlayer.getInventory().getItem(payload.slot()); ItemStack stack = entry.location().getStack(serverPlayer);
BagCompat.BagHandler handler = BagCompat.findHandler(stack); BagCompat.BagHandler handler = entry.handler();
if (handler != null) { 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, 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, BagEntry entry) {
Inventory inventory = player.getInventory();
ItemStack sourceStack = entry.location().getStack(player);
if (sourceStack.isEmpty()) {
return false;
}
int targetHotbarSlot = chooseHotbarSlot(inventory, entry.slot());
if (targetHotbarSlot < 0) {
return false;
}
if (!entry.location().moveToHotbar(player, targetHotbarSlot)) {
return false;
}
inventory.selected = targetHotbarSlot;
player.connection.send(new ClientboundSetCarriedItemPacket(targetHotbarSlot));
player.inventoryMenu.broadcastChanges();
ItemStack mainHandStack = player.getItemInHand(InteractionHand.MAIN_HAND);
if (mainHandStack.isEmpty()) {
return false;
}
int previousContainerId = player.containerMenu.containerId;
InteractionResult result = player.gameMode.useItem(player, player.level(), mainHandStack, InteractionHand.MAIN_HAND);
player.inventoryMenu.broadcastChanges();
return result.consumesAction() || player.containerMenu.containerId != previousContainerId;
}
private static int chooseHotbarSlot(Inventory inventory, int sourceSlot) {
if (sourceSlot >= 0 && sourceSlot < Inventory.getSelectionSize()) {
return sourceSlot;
}
for (int hotbarSlot = 0; hotbarSlot < Inventory.getSelectionSize(); hotbarSlot++) {
if (inventory.getItem(hotbarSlot).isEmpty()) {
return hotbarSlot;
}
}
for (int hotbarSlot = 0; hotbarSlot < Inventory.getSelectionSize(); hotbarSlot++) {
if (isBuildingBlock(inventory.getItem(hotbarSlot))) {
return hotbarSlot;
}
}
for (int hotbarSlot = 0; hotbarSlot < Inventory.getSelectionSize(); hotbarSlot++) {
if (isPreferredMiscItem(inventory.getItem(hotbarSlot))) {
return hotbarSlot;
}
}
return -1;
}
private static boolean isBuildingBlock(ItemStack stack) {
return !stack.isEmpty() && stack.getItem() instanceof BlockItem && !isTorch(stack.getItem());
}
private static boolean isPreferredMiscItem(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
Item item = stack.getItem();
return !stack.has(DataComponents.FOOD)
&& !isTorch(item)
&& !(item instanceof DiggerItem)
&& !(item instanceof SwordItem)
&& !(item instanceof BowItem)
&& !(item instanceof CrossbowItem)
&& !(item instanceof TridentItem)
&& !(item instanceof ShieldItem)
&& !(item instanceof ShearsItem)
&& !(item instanceof ProjectileWeaponItem);
}
private static boolean isTorch(Item item) {
return item instanceof BlockItem blockItem
&& blockItem.getBlock().getDescriptionId().toLowerCase(java.util.Locale.ROOT).contains("torch");
} }
} }

View File

@@ -37,8 +37,11 @@ public record QueryInsertTargetsPayload(int requestId) implements CustomPacketPa
if (!carriedStack.isEmpty()) { if (!carriedStack.isEmpty()) {
for (BagEntry bag : BagAccess.findBags(serverPlayer)) { for (BagEntry bag : BagAccess.findBags(serverPlayer)) {
ItemStack bagStack = serverPlayer.getInventory().getItem(bag.slot()); if (bag.handler() == null) {
if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bagStack, carriedStack)) { continue;
}
ItemStack bagStack = bag.location().getStack(serverPlayer);
if (bag.handler().canInsertFromCarried(serverPlayer, bag.slot(), bag.location(), bagStack, carriedStack)) {
insertableSlots.add(bag.slot()); insertableSlots.add(bag.slot());
} }
} }

View File

@@ -5,7 +5,39 @@
"container.bagtabs.bag": "Bag", "container.bagtabs.bag": "Bag",
"container.bagtabs.bag_namer": "Bag Namer", "container.bagtabs.bag_namer": "Bag Namer",
"bagtabs.tooltip.click_to_open": "Open from your inventory tabs", "bagtabs.tooltip.click_to_open": "Open from your inventory tabs",
"bagtabs.tooltip.hotbar_fallback": "This container is not fully supported",
"bagtabs.gui.bag_namer.name": "New Name", "bagtabs.gui.bag_namer.name": "New Name",
"bagtabs.gui.bag_namer.placeholder": "Leave blank to clear", "bagtabs.gui.bag_namer.placeholder": "Leave blank to clear",
"bagtabs.gui.bag_namer.rename": "Rename" "bagtabs.gui.bag_namer.rename": "Rename",
"bagtabs.dock.title": "Tab Dock Settings",
"bagtabs.dock.done": "Done",
"bagtabs.dock.reset": "Reset Screen Override",
"bagtabs.dock.target.global": "Editing: Global Default",
"bagtabs.dock.target.override": "Editing: This Screen Override",
"bagtabs.dock.x_offset": "X Offset",
"bagtabs.dock.y_offset": "Y Offset",
"bagtabs.dock.max_tabs": "Max Tabs",
"bagtabs.dock.compact.on": "Layout: Compact",
"bagtabs.dock.compact.off": "Layout: Normal",
"bagtabs.dock.fullness.on": "Fullness: Shown",
"bagtabs.dock.fullness.off": "Fullness: Hidden",
"bagtabs.dock.open": "Open dock settings",
"bagtabs.dock.lock": "Lock tab interactions",
"bagtabs.dock.unlock": "Unlock tab interactions",
"bagtabs.dock.scroll_prev": "Show previous tabs",
"bagtabs.dock.scroll_next": "Show more tabs",
"bagtabs.dock.offset_steps": "Click: 1, Shift: 5, Ctrl: 25, Ctrl+Shift: 100",
"bagtabs.dock.side.bottom": "Dock: Bottom",
"bagtabs.dock.side.top": "Dock: Top",
"bagtabs.dock.side.left": "Dock: Left",
"bagtabs.dock.side.right": "Dock: Right",
"bagtabs.dock.side.screen_bottom": "Dock: Screen Bottom",
"bagtabs.dock.side.screen_top": "Dock: Screen Top",
"bagtabs.dock.side.screen_left": "Dock: Screen Left",
"bagtabs.dock.side.screen_right": "Dock: Screen Right",
"bagtabs.dock.side.floating_horizontal": "Dock: Floating Horizontal",
"bagtabs.dock.side.floating_vertical": "Dock: Floating Vertical",
"bagtabs.generic_open_failed": "No safe hotbar slot was available to open that bag.",
"key.categories.bagtabs": "Bag Tabs",
"key.bagtabs.open_last_bag": "Open Last Bag"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 B

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 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;
}
}
}