pin bags
This commit is contained in:
@@ -22,7 +22,7 @@ public final class BagAccess {
|
|||||||
}
|
}
|
||||||
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
BagCompat.BagHandler handler = BagCompat.findHandler(stack);
|
||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
bags.add(new BagEntry(slot, stack.copy(), handler));
|
bags.add(new BagEntry(slot, stack.copy(), handler, BagCompat.getIdentity(stack)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import net.minecraft.client.Minecraft;
|
|||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||||
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
|
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.resources.ResourceLocation;
|
||||||
import net.minecraft.sounds.SoundEvents;
|
import net.minecraft.sounds.SoundEvents;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
@@ -68,19 +70,24 @@ public final class BagTabOverlay {
|
|||||||
|
|
||||||
for (RenderedTab tab : tabs) {
|
for (RenderedTab tab : tabs) {
|
||||||
renderTab(guiGraphics, tab, mouseX, mouseY, carriedStack);
|
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) {
|
for (RenderedTab tab : tabs) {
|
||||||
if (tab.isHovered(mouseX, mouseY)) {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void mouseClicked(ScreenEvent.MouseButtonPressed.Pre event) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +96,31 @@ public final class BagTabOverlay {
|
|||||||
return;
|
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;
|
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())) {
|
if (tab.isHovered(event.getMouseX(), event.getMouseY())) {
|
||||||
PacketDistributor.sendToServer(new OpenBagPayload(tab.entry().slot()));
|
PacketDistributor.sendToServer(new OpenBagPayload(tab.entry().slot()));
|
||||||
Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
|
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) {
|
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<>();
|
List<RenderedTab> renderedTabs = new ArrayList<>();
|
||||||
int activeBagSlot = getActiveBagSlot(screen);
|
int activeBagSlot = getActiveBagSlot(screen);
|
||||||
int leftBound = getInventoryLeftBound(screen, player);
|
int leftBound = getInventoryLeftBound(screen, player);
|
||||||
@@ -160,7 +187,8 @@ public final class BagTabOverlay {
|
|||||||
break;
|
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;
|
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);
|
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 (!carriedStack.isEmpty()) {
|
||||||
if (INSERTABLE_SLOTS.contains(tab.entry().slot())) {
|
if (INSERTABLE_SLOTS.contains(tab.entry().slot())) {
|
||||||
renderPlusIndicator(guiGraphics, tab.x() + 15, tab.y() + 3);
|
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);
|
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) {
|
private boolean isHovered(double mouseX, double mouseY) {
|
||||||
return mouseX >= this.x && mouseX < this.x + TAB_WIDTH && mouseY >= this.y && mouseY < this.y + TAB_HEIGHT;
|
return mouseX >= this.x && mouseX < this.x + TAB_WIDTH && mouseY >= this.y && mouseY < this.y + TAB_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|||||||
178
src/main/java/com/trunksbomb/bagtabs/client/TabPinManager.java
Normal file
178
src/main/java/com/trunksbomb/bagtabs/client/TabPinManager.java
Normal 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user