fix(sanctuary): trier coffres et barrels et garder la hotbar en bas #120

Merged
koka merged 2 commits from fix/issue-118-container-sort into main 2026-08-31 04:15:43 +00:00
12 changed files with 127 additions and 17 deletions
+4 -2
View File
@@ -7062,8 +7062,10 @@ tasks.register("verifyAlpha223Release") {
|| !sortClient.contains("instanceof CreativeModeInventoryScreen")
|| !sortClient.contains("playButtonClickSound")
|| !sortPayload.contains('SanctuaryMod.id("c2s/sort_inventory")')
|| !sortPayload.contains("Intentionally empty")
|| !networking.contains("SanctuaryInventorySort.sort(context.player())")
|| !sortPayload.contains("buffer.writeBoolean(container)")
|| !inventorySort.contains("sortOpenContainer")
|| !inventorySort.contains("instanceof ChestMenu")
|| !networking.contains("SanctuaryInventorySort.sort(context.player(), payload.container())")
|| !networking.contains("PROTOCOL_VERSION = 22")
|| !displayMode.contains('TEXT("text")')
|| !displayMode.contains('PANEL("panel")')
+8
View File
@@ -639,6 +639,14 @@ tasks.register("verifySanctuary") {
,'matchesMouse'
,'int bankIndex(int visualRow, int visibleRows)'
,'playButtonClickSound'
,'requestVisualRow(client, 0, currentRows)'
,'isSortableStorageMenu'
,'instanceof ChestMenu'
,'instanceof ShulkerBoxMenu'
,'instanceof HopperMenu'
,'instanceof DispenserMenu'
,'buffer.writeBoolean(container)'
,'sortOpenContainer'
,'GLFW.GLFW_KEY_TAB'
,'HOTBAR_TEXTURE'
,'0.0F, 0.0F, 182, 22, 182, 22'
@@ -33,7 +33,9 @@ public final class SanctuaryClientProgress {
Minecraft client = Minecraft.getInstance();
if (currentRows > 1) {
SanctuaryHotbarRowsClient.requestVisualRow(client, currentRows - 1, currentRows);
// Visual 0 is the bottom bank line after bankIndex inversion. Keep the
// live hotbar there on login and after an inventory upgrade.
SanctuaryHotbarRowsClient.requestVisualRow(client, 0, currentRows);
}
if (client.gui.screen() instanceof AbstractContainerScreen<?> screen) {
screen.resize(client.getWindow().getGuiScaledWidth(),
@@ -44,7 +44,7 @@ public final class SanctuaryHotbarRowsClient {
return requestVisualRow(client, targetVisualRow, unlockedRows);
}
/** Makes the requested visible row the real hotbar, notably after an inventory upgrade. */
/** Makes the requested visible row the real hotbar. Visual 0 is the bottom line. */
public static boolean requestVisualRow(Minecraft client, int targetVisualRow, int unlockedRows) {
unlockedRows = Math.clamp(unlockedRows, 1, SanctuaryInventoryCapacity.MAX_ROW_COUNT);
if (pending || unlockedRows <= 1 || client.player == null || client.getConnection() == null
@@ -1,5 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.client;
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySort;
import fr.koka99cab.sanctuary26.sanctuary.mixin.client.AbstractContainerScreenAccessor;
import fr.koka99cab.sanctuary26.sanctuary.network.SortInventoryPayload;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.Minecraft;
@@ -7,6 +9,8 @@ import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.gui.screens.inventory.CreativeModeInventoryScreen;
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
import net.minecraft.world.inventory.Slot;
/** Sends a sort request while a survival container is open. */
public final class SanctuaryInventorySortClient {
@@ -17,18 +21,35 @@ public final class SanctuaryInventorySortClient {
if (client.gui.screen() != null && client.gui.screen().getFocused() instanceof EditBox) {
return false;
}
return request(client);
return request(client, null);
}
public static boolean request(Minecraft client) {
return request(client, null);
}
public static boolean request(Minecraft client, Slot underCursor) {
if (client.player == null || client.level == null
|| !(client.gui.screen() instanceof AbstractContainerScreen<?>)
|| client.gui.screen() instanceof CreativeModeInventoryScreen
|| !(client.gui.screen() instanceof AbstractContainerScreen<?> screen)
|| screen instanceof CreativeModeInventoryScreen
|| !ClientPlayNetworking.canSend(SortInventoryPayload.ID)) {
return false;
}
boolean container = resolveContainerTarget(screen, underCursor);
AbstractWidget.playButtonClickSound(client.getSoundManager());
ClientPlayNetworking.send(new SortInventoryPayload());
ClientPlayNetworking.send(new SortInventoryPayload(container));
return true;
}
private static boolean resolveContainerTarget(AbstractContainerScreen<?> screen,
Slot underCursor) {
if (screen instanceof InventoryScreen) return false;
Slot hovered = underCursor != null ? underCursor
: ((AbstractContainerScreenAccessor) screen).sanctuary$getHoveredSlot();
if (hovered != null) {
return SanctuaryInventorySort.isSortableStorageMenu(screen.getMenu())
&& !SanctuaryInventorySort.isPlayerInventorySlot(hovered);
}
return SanctuaryInventorySort.isSortableStorageMenu(screen.getMenu());
}
}
@@ -10,11 +10,18 @@ import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.DispenserMenu;
import net.minecraft.world.inventory.HopperMenu;
import net.minecraft.world.inventory.ShulkerBoxMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
/**
* Server-authoritative sort of every unlocked inventory row except the live hotbar.
* Armor, off-hand, crafting, cape and companion slots stay untouched.
* Server-authoritative sort of player storage or an open chest-like container.
* Armor, off-hand, crafting, cape, companion slots and the live hotbar stay
* untouched. Furnaces, shops and the storage terminal are never rewritten.
*/
public final class SanctuaryInventorySort {
private static final int COOLDOWN_TICKS = 8;
@@ -85,11 +92,31 @@ public final class SanctuaryInventorySort {
ServerPlayerEvents.LEAVE.register(player -> LAST_SORT_TICK.remove(player.getUUID()));
}
public static void sort(ServerPlayer player) {
public static boolean isSortableStorageMenu(AbstractContainerMenu menu) {
return menu instanceof ChestMenu
|| menu instanceof ShulkerBoxMenu
|| menu instanceof HopperMenu
|| menu instanceof DispenserMenu;
}
public static boolean isPlayerInventorySlot(Slot slot) {
if (slot == null) return false;
Container container = slot.container;
return container instanceof Inventory
|| container instanceof SanctuaryOverflowContainer
|| container instanceof SanctuaryCapeContainer
|| container instanceof SanctuaryCompanionEggContainer;
}
public static void sort(ServerPlayer player, boolean container) {
long now = player.level().getGameTime();
Long previous = LAST_SORT_TICK.get(player.getUUID());
if (previous != null && now - previous < COOLDOWN_TICKS) return;
LAST_SORT_TICK.put(player.getUUID(), now);
if (container) {
sortOpenContainer(player);
return;
}
int unlockedRows = SanctuaryInventoryCapacity.unlockedRows(player);
if (unlockedRows <= 1) return;
@@ -123,6 +150,28 @@ public final class SanctuaryInventorySort {
if (player.containerMenu != player.inventoryMenu) player.containerMenu.broadcastChanges();
}
private static void sortOpenContainer(ServerPlayer player) {
AbstractContainerMenu menu = player.containerMenu;
if (menu == null || menu == player.inventoryMenu || !isSortableStorageMenu(menu)
|| !menu.stillValid(player)) {
return;
}
List<Slot> sortable = new ArrayList<>();
for (Slot slot : menu.slots) {
if (!slot.isActive() || isPlayerInventorySlot(slot)) continue;
sortable.add(slot);
}
if (sortable.isEmpty()) return;
List<ItemStack> items = new ArrayList<>(sortable.size());
for (Slot slot : sortable) items.add(slot.getItem().copy());
SanctuaryInventorySorter.compactAndSort(items, STACKS);
for (int index = 0; index < sortable.size(); index++) {
Slot slot = sortable.get(index);
slot.container.setItem(slot.getContainerSlot(), items.get(index));
}
menu.broadcastChanges();
}
private static ItemStack getRowItem(Inventory inventory, Container overflow,
int logicalRow, int column) {
if (logicalRow < SanctuaryInventoryCapacity.VANILLA_ROW_COUNT) {
@@ -1,6 +1,7 @@
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.inventory.Slot;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.gen.Accessor;
@@ -21,4 +22,7 @@ public interface AbstractContainerScreenAccessor {
@Mutable
@Accessor("imageHeight")
void sanctuary$setImageHeight(int imageHeight);
@Accessor("hoveredSlot")
Slot sanctuary$getHoveredSlot();
}
@@ -251,7 +251,8 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
private void sanctuary$sortInventory(MouseButtonEvent event, boolean doubleClick,
CallbackInfoReturnable<Boolean> cir) {
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
if (SanctuaryInventorySortClient.request(Minecraft.getInstance())) {
if (SanctuaryInventorySortClient.request(Minecraft.getInstance(),
sanctuary$slotAt(event.x(), event.y()))) {
cir.setReturnValue(true);
}
}
@@ -6,6 +6,7 @@ import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryInventorySortClient;
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryZoom;
import net.minecraft.client.Minecraft;
import net.minecraft.client.MouseHandler;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.client.input.MouseButtonInfo;
import org.lwjgl.glfw.GLFW;
@@ -32,6 +33,9 @@ public abstract class MouseHandlerMixin {
if (action != GLFW.GLFW_PRESS) return;
Minecraft client = Minecraft.getInstance();
if (windowPointer != client.getWindow().handle()) return;
if (client.gui.screen() instanceof AbstractContainerScreen<?>) {
return;
}
MouseButtonEvent event = new MouseButtonEvent(0.0, 0.0, buttonInfo);
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
if (SanctuaryInventorySortClient.request(client)) {
@@ -75,7 +75,8 @@ public final class SanctuaryNetworking {
}
}));
ServerPlayNetworking.registerGlobalReceiver(SortInventoryPayload.ID, (payload, context) ->
context.server().execute(() -> SanctuaryInventorySort.sort(context.player())));
context.server().execute(() ->
SanctuaryInventorySort.sort(context.player(), payload.container())));
ServerPlayNetworking.registerGlobalReceiver(CompanionPowerUsePayload.ID, (payload, context) ->
context.server().execute(() -> SanctuaryCompanionPowers.use(context.player())));
ServerPlayNetworking.registerGlobalReceiver(SitRequestPayload.ID, (payload, context) ->
@@ -5,19 +5,22 @@ import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
/** A parameterless client intent. The server owns which unlocked rows are sorted. */
public record SortInventoryPayload() implements CustomPacketPayload {
/**
* Bounded client intent: player storage or the open storage container.
* The server still decides which slots exist and ignores non-storage menus.
*/
public record SortInventoryPayload(boolean container) implements CustomPacketPayload {
public static final Type<SortInventoryPayload> ID =
new Type<>(SanctuaryMod.id("c2s/sort_inventory"));
public static final StreamCodec<RegistryFriendlyByteBuf, SortInventoryPayload> CODEC =
CustomPacketPayload.codec(SortInventoryPayload::write, SortInventoryPayload::new);
private SortInventoryPayload(RegistryFriendlyByteBuf buffer) {
this();
this(buffer.readBoolean());
}
private void write(RegistryFriendlyByteBuf buffer) {
// Intentionally empty: no client-supplied slot list or item identity can be trusted.
buffer.writeBoolean(container);
}
@Override
@@ -43,6 +43,21 @@ public final class SanctuaryInventorySortSmoke {
"Dirt remainder must keep the leftover count");
require(storage.get(6).isEmpty(), "Unused storage slots must end empty");
List<FakeStack> chest = new ArrayList<>(List.of(
FakeStack.EMPTY,
new FakeStack("minecraft:oak_log", "", 16, 64),
new FakeStack("minecraft:oak_log", "", 50, 64),
new FakeStack("minecraft:arrow", "", 32, 64)
));
SanctuaryInventorySorter.compactAndSort(chest, access);
require(chest.get(0).id.equals("minecraft:arrow") && chest.get(0).count == 32,
"A chest-like list must compact from the first slot");
require(chest.get(1).id.equals("minecraft:oak_log") && chest.get(1).count == 64,
"Identical chest stacks must merge up to max size");
require(chest.get(2).id.equals("minecraft:oak_log") && chest.get(2).count == 2,
"Chest remainder must follow the merged stack");
require(chest.get(3).isEmpty(), "Unused chest slots must end empty");
List<FakeStack> hotbar = new ArrayList<>(List.of(
new FakeStack("minecraft:torch", "", 8, 64),
new FakeStack("minecraft:bread", "", 3, 64)