This commit is contained in:
trunksbomb
2026-03-22 20:10:32 -04:00
parent f40b38ab3c
commit 15be03e055
4 changed files with 239 additions and 10 deletions

View File

@@ -22,7 +22,7 @@ public final class BagAccess {
}
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
if (handler != null) {
bags.add(new BagEntry(slot, stack.copy(), handler));
bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack)));
}
}

View File

@@ -2,5 +2,5 @@ package com.trunksbomb.bagtabs.bag;
import net.minecraft.world.item.ItemStack;
public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler) {
public record BagEntry(int slot, ItemStack stack, BagCompat.BagHandler handler, BagIdentity identity) {
}

View File

@@ -18,6 +18,8 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.entity.player.Player;
@@ -68,19 +70,24 @@ public final class BagTabOverlay {
for (RenderedTab tab : tabs) {
renderTab(guiGraphics, tab, mouseX, mouseY, carriedStack);
guiGraphics.renderItem(tab.entry().stack(), tab.x() + 3, tab.y() + 3);
guiGraphics.renderItem(tab.entry().stack(), tab.x() + 3, tab.y() + 2);
}
for (RenderedTab tab : tabs) {
if (tab.isHovered(mouseX, mouseY)) {
guiGraphics.renderTooltip(Minecraft.getInstance().font, tab.entry().stack().getHoverName(), mouseX, mouseY);
guiGraphics.renderTooltip(
Minecraft.getInstance().font,
getTooltipLines(tab, tabs).stream().map(Component::getVisualOrderText).toList(),
mouseX,
mouseY
);
break;
}
}
}
public static void mouseClicked(ScreenEvent.MouseButtonPressed.Pre event) {
if (!(event.getScreen() instanceof AbstractContainerScreen<?> screen) || !supportsTabs(screen) || event.getButton() != 0) {
if (!(event.getScreen() instanceof AbstractContainerScreen<?> screen) || !supportsTabs(screen)) {
return;
}
@@ -89,11 +96,31 @@ public final class BagTabOverlay {
return;
}
if (!screen.getMenu().getCarried().isEmpty()) {
List<RenderedTab> tabs = getRenderedTabs(screen, player);
if (event.getButton() == 1) {
for (RenderedTab tab : tabs) {
if (!tab.isHovered(event.getMouseX(), event.getMouseY())) {
continue;
}
TabPinManager.ToggleResult result = TabPinManager.togglePin(tab.entry(), tabs.stream().map(RenderedTab::entry).toList());
if (result.changed()) {
Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
} else {
Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.VILLAGER_NO, 0.8F));
}
event.setCanceled(true);
return;
}
return;
}
for (RenderedTab tab : getRenderedTabs(screen, player)) {
if (event.getButton() != 0 || !screen.getMenu().getCarried().isEmpty()) {
return;
}
for (RenderedTab tab : tabs) {
if (tab.isHovered(event.getMouseX(), event.getMouseY())) {
PacketDistributor.sendToServer(new OpenBagPayload(tab.entry().slot()));
Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
@@ -146,7 +173,7 @@ public final class BagTabOverlay {
}
private static List<RenderedTab> getRenderedTabs(AbstractContainerScreen<?> screen, Player player) {
List<BagEntry> bags = BagAccess.findBags(player);
List<BagEntry> bags = TabPinManager.sortTabs(BagAccess.findBags(player));
List<RenderedTab> renderedTabs = new ArrayList<>();
int activeBagSlot = getActiveBagSlot(screen);
int leftBound = getInventoryLeftBound(screen, player);
@@ -160,7 +187,8 @@ public final class BagTabOverlay {
break;
}
renderedTabs.add(new RenderedTab(bag, x, y, bag.slot() == activeBagSlot));
boolean pinned = TabPinManager.isPinned(bag, bags);
renderedTabs.add(new RenderedTab(bag, x, y, bag.slot() == activeBagSlot, pinned));
x += TAB_WIDTH + TAB_GAP;
}
@@ -206,6 +234,12 @@ public final class BagTabOverlay {
guiGraphics.fill(tab.x() + 2, tab.y() + 2, tab.x() + TAB_WIDTH - 2, tab.y() + 4, 0x90FFFFFF);
}
if (tab.pinned()) {
guiGraphics.fill(tab.x() + 2, tab.y() + 4, tab.x() + 6, tab.y() + 5, 0xFFFFDA6B);
guiGraphics.fill(tab.x() + 3, tab.y() + 5, tab.x() + 5, tab.y() + 8, 0xFFFFDA6B);
guiGraphics.fill(tab.x() + 4, tab.y() + 8, tab.x() + 5, tab.y() + 9, 0xFFD94A4A);
}
if (!carriedStack.isEmpty()) {
if (INSERTABLE_SLOTS.contains(tab.entry().slot())) {
renderPlusIndicator(guiGraphics, tab.x() + 15, tab.y() + 3);
@@ -264,7 +298,24 @@ public final class BagTabOverlay {
guiGraphics.fill(x + 4, y + 4, x + 5, y + 5, 0xFFFF6C6C);
}
private record RenderedTab(BagEntry entry, int x, int y, boolean selected) {
private static List<Component> getTooltipLines(RenderedTab tab, List<RenderedTab> tabs) {
List<Component> lines = new ArrayList<>();
lines.add(tab.entry().stack().getHoverName());
lines.add(Component.literal("Left-click: open"));
if (tab.pinned()) {
lines.add(Component.literal("Right-click: unpin"));
} else {
String failureReason = TabPinManager.getPinFailureReason(tab.entry(), tabs.stream().map(RenderedTab::entry).toList());
if (failureReason == null) {
lines.add(Component.literal("Right-click: pin"));
} else {
lines.add(Component.literal("Rename bag to pin").withStyle(ChatFormatting.ITALIC));
}
}
return lines;
}
private record RenderedTab(BagEntry entry, int x, int y, boolean selected, boolean pinned) {
private boolean isHovered(double mouseX, double mouseY) {
return mouseX >= this.x && mouseX < this.x + TAB_WIDTH && mouseY >= this.y && mouseY < this.y + TAB_HEIGHT;
}

View File

@@ -0,0 +1,178 @@
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);
}
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) {
}
}