Compare commits
13 Commits
f40b38ab3c
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5c35871d | ||
|
|
5d283909db | ||
|
|
8189359e91 | ||
|
|
a90227ff02 | ||
|
|
6271417381 | ||
|
|
274e4f6dec | ||
|
|
e201c8c674 | ||
|
|
966e199a23 | ||
|
|
56fcb49f4d | ||
|
|
638fce4116 | ||
|
|
b51b41206c | ||
|
|
85a7e1a48b | ||
|
|
15be03e055 |
43
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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: Make Gradle wrapper executable
|
||||||
|
run: chmod +x ./gradlew
|
||||||
|
|
||||||
|
- name: Build release jar
|
||||||
|
run: ./gradlew --no-daemon clean build
|
||||||
|
|
||||||
|
- name: Publish release artifacts
|
||||||
|
run: python3 ./scripts/publish_release.py
|
||||||
109
README.md
@@ -1,25 +1,96 @@
|
|||||||
|
# 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.
|
- 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/
|
- 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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
409
net/minecraft/server/level/ServerPlayerGameMode.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1031
net/neoforged/neoforge/client/event/ScreenEvent.java
Normal file
BIN
project_picture.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
project_picture_full.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
397
scripts/publish_release.py
Normal 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()
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ 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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 +15,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(
|
||||||
@@ -48,6 +50,10 @@ public final class BagCompat {
|
|||||||
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;
|
||||||
@@ -83,6 +89,24 @@ public final class BagCompat {
|
|||||||
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);
|
||||||
|
|
||||||
@@ -99,6 +123,10 @@ public final class BagCompat {
|
|||||||
default int getActiveBagSlot(AbstractContainerMenu menu) {
|
default int getActiveBagSlot(AbstractContainerMenu menu) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default float getFullness(ItemStack stack) {
|
||||||
|
return -1.0F;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class NativeBagHandler implements BagHandler {
|
private static final class NativeBagHandler implements BagHandler {
|
||||||
@@ -139,6 +167,11 @@ public final class BagCompat {
|
|||||||
|
|
||||||
return BagContainer.canInsertInto(new BagContainer(player, slot), carriedStack);
|
return BagContainer.canInsertInto(new BagContainer(player, slot), carriedStack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getFullness(ItemStack stack) {
|
||||||
|
return getContainerFullness(stack.getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Integer getDankFrequency(ItemStack stack) {
|
private static Integer getDankFrequency(ItemStack stack) {
|
||||||
@@ -227,6 +260,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 {
|
||||||
@@ -346,6 +391,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 {
|
||||||
@@ -425,6 +475,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 +674,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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/main/java/com/trunksbomb/bagtabs/client/TabPinManager.java
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,27 @@ package com.trunksbomb.bagtabs.network;
|
|||||||
|
|
||||||
import com.trunksbomb.bagtabs.BagTabs;
|
import com.trunksbomb.bagtabs.BagTabs;
|
||||||
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 {
|
||||||
@@ -37,6 +51,99 @@ public record OpenBagPayload(int slot) implements CustomPacketPayload {
|
|||||||
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
handler.open(serverPlayer, payload.slot(), stack);
|
handler.open(serverPlayer, payload.slot(), stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BagCompat.canShowInTabs(stack)) {
|
||||||
|
if (!openViaHotbarFallback(serverPlayer, payload.slot())) {
|
||||||
|
serverPlayer.sendSystemMessage(BagTabs.translation("generic_open_failed"));
|
||||||
|
BagTabs.LOGGER.debug("Failed to open bag fallback for item {} in slot {}", stack, payload.slot());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean openViaHotbarFallback(ServerPlayer player, int sourceSlot) {
|
||||||
|
Inventory inventory = player.getInventory();
|
||||||
|
ItemStack sourceStack = inventory.getItem(sourceSlot);
|
||||||
|
if (sourceStack.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetHotbarSlot = chooseHotbarSlot(inventory, sourceSlot);
|
||||||
|
if (targetHotbarSlot < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceSlot != targetHotbarSlot) {
|
||||||
|
ItemStack hotbarStack = inventory.getItem(targetHotbarSlot).copy();
|
||||||
|
inventory.setItem(targetHotbarSlot, sourceStack.copy());
|
||||||
|
inventory.setItem(sourceSlot, hotbarStack);
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 426 B |
|
After Width: | Height: | Size: 399 B |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 423 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/bag_tabs_dock.png
Normal file
|
After Width: | Height: | Size: 429 B |
|
Before Width: | Height: | Size: 227 B After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 453 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/dock_gear.png
Normal file
|
After Width: | Height: | Size: 592 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/dock_lock.png
Normal file
|
After Width: | Height: | Size: 470 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/dock_next.png
Normal file
|
After Width: | Height: | Size: 491 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/dock_prev.png
Normal file
|
After Width: | Height: | Size: 496 B |
BIN
src/main/resources/assets/bagtabs/textures/gui/dock_unlock.png
Normal file
|
After Width: | Height: | Size: 477 B |