feat(sanctuary): trier le stockage sans deplacer la hotbar #112
@@ -108,6 +108,14 @@ tasks.register("capeJoinSyncSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke"
|
||||
}
|
||||
|
||||
tasks.register("inventorySortSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks that storage stacks merge and sort while the hotbar row stays out of the sort."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySortSmoke"
|
||||
}
|
||||
|
||||
tasks.register("alphaIndevGeneratorSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks deterministic finite Indev islands, residences and safe portal arrivals."
|
||||
@@ -298,6 +306,7 @@ tasks.register("verifySanctuary") {
|
||||
tasks.named("questIdSuggestionsSmoke"),
|
||||
tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("capeJoinSyncSmoke"),
|
||||
tasks.named("inventorySortSmoke"),
|
||||
tasks.named("verifyBlackMarket")
|
||||
|
||||
doLast {
|
||||
@@ -614,6 +623,14 @@ tasks.register("verifySanctuary") {
|
||||
,'class SanctuaryHotbarRowsClient'
|
||||
,'id("c2s/hotbar_row_switch")'
|
||||
,'id("s2c/hotbar_row_sync")'
|
||||
,'class SanctuaryInventorySort'
|
||||
,'class SanctuaryInventorySorter'
|
||||
,'id("c2s/sort_inventory")'
|
||||
,'key.sanctuary.sort_inventory'
|
||||
,'GLFW.GLFW_MOUSE_BUTTON_MIDDLE'
|
||||
,'matchesMouse'
|
||||
,'int bankIndex(int visualRow, int visibleRows)'
|
||||
,'playButtonClickSound'
|
||||
,'GLFW.GLFW_KEY_TAB'
|
||||
,'HOTBAR_TEXTURE'
|
||||
,'0.0F, 0.0F, 182, 22, 182, 22'
|
||||
@@ -3423,5 +3440,7 @@ tasks.named("check") {
|
||||
dependsOn tasks.named("questTrackerModelSmoke")
|
||||
dependsOn tasks.named("questIdSuggestionsSmoke")
|
||||
dependsOn tasks.named("alphaIndevGeneratorSmoke")
|
||||
dependsOn tasks.named("capeJoinSyncSmoke")
|
||||
dependsOn tasks.named("inventorySortSmoke")
|
||||
dependsOn tasks.named("verifyAlphaIndev")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryBulkActions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventoryCapacity;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCompanions;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryHotbarRows;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySort;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCompanionPowers;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCapeService;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuarySitting;
|
||||
@@ -55,6 +56,7 @@ public final class SanctuaryMod implements ModInitializer {
|
||||
SanctuaryBulkActions.initialize();
|
||||
SanctuaryInventoryCapacity.initialize();
|
||||
SanctuaryHotbarRows.initialize();
|
||||
SanctuaryInventorySort.initialize();
|
||||
SanctuaryCompanions.initialize();
|
||||
SanctuaryCompanionPowers.initialize();
|
||||
SanctuaryCapeService.initialize();
|
||||
|
||||
+16
@@ -28,6 +28,7 @@ import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.Screenshot;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.gui.screens.inventory.ContainerScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.CraftingScreen;
|
||||
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
|
||||
@@ -49,6 +50,7 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
private static KeyMapping zoomKey;
|
||||
private static KeyMapping celestialKey;
|
||||
private static KeyMapping switchHotbarRowKey;
|
||||
private static KeyMapping sortInventoryKey;
|
||||
private static KeyMapping questTrackerKey;
|
||||
private static boolean zoomWasHeld;
|
||||
private static boolean smokeScreenOpened;
|
||||
@@ -93,6 +95,12 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
GLFW.GLFW_KEY_TAB,
|
||||
SANCTUARY_CATEGORY
|
||||
));
|
||||
sortInventoryKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.sanctuary.sort_inventory",
|
||||
InputConstants.Type.MOUSE,
|
||||
GLFW.GLFW_MOUSE_BUTTON_MIDDLE,
|
||||
SANCTUARY_CATEGORY
|
||||
));
|
||||
questTrackerKey = KeyMappingHelper.registerKeyMapping(new KeyMapping(
|
||||
"key.sanctuary.quest_tracker",
|
||||
InputConstants.Type.KEYSYM,
|
||||
@@ -222,6 +230,14 @@ public final class SanctuaryClient implements ClientModInitializer {
|
||||
return switchHotbarRowKey != null && switchHotbarRowKey.matches(event);
|
||||
}
|
||||
|
||||
public static boolean isSortInventoryKey(KeyEvent event) {
|
||||
return sortInventoryKey != null && sortInventoryKey.matches(event);
|
||||
}
|
||||
|
||||
public static boolean isSortInventoryMouse(MouseButtonEvent event) {
|
||||
return sortInventoryKey != null && sortInventoryKey.matchesMouse(event);
|
||||
}
|
||||
|
||||
public static boolean handleBlackMarketMiddleClick(Minecraft client) {
|
||||
if (client.player == null || client.level == null || client.gui.screen() != null
|
||||
|| !(client.hitResult instanceof BlockHitResult hit)
|
||||
|
||||
+4
@@ -24,6 +24,10 @@ public final class SanctuaryHotbarRowsClient {
|
||||
return VISUAL_ROW_BY_LOGICAL[logicalRow];
|
||||
}
|
||||
|
||||
public static int bankIndex(int logicalRow, int visibleRows) {
|
||||
return SanctuaryInventoryCapacity.bankIndex(visualRow(logicalRow), visibleRows);
|
||||
}
|
||||
|
||||
public static int activeVisualRow() {
|
||||
return VISUAL_ROW_BY_LOGICAL[0];
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.SortInventoryPayload;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
|
||||
import net.minecraft.client.Minecraft;
|
||||
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;
|
||||
|
||||
/** Sends a sort request while a survival container is open. */
|
||||
public final class SanctuaryInventorySortClient {
|
||||
private SanctuaryInventorySortClient() {
|
||||
}
|
||||
|
||||
public static boolean requestFromKeyboard(Minecraft client) {
|
||||
if (client.gui.screen() != null && client.gui.screen().getFocused() instanceof EditBox) {
|
||||
return false;
|
||||
}
|
||||
return request(client);
|
||||
}
|
||||
|
||||
public static boolean request(Minecraft client) {
|
||||
if (client.player == null || client.level == null
|
||||
|| !(client.gui.screen() instanceof AbstractContainerScreen<?>)
|
||||
|| client.gui.screen() instanceof CreativeModeInventoryScreen
|
||||
|| !ClientPlayNetworking.canSend(SortInventoryPayload.ID)) {
|
||||
return false;
|
||||
}
|
||||
AbstractWidget.playButtonClickSound(client.getSoundManager());
|
||||
ClientPlayNetworking.send(new SortInventoryPayload());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+6
@@ -88,6 +88,12 @@ public final class SanctuaryHotbarRows {
|
||||
return layout;
|
||||
}
|
||||
|
||||
/** Visual row of each physical row. Index 0 is the live hotbar. */
|
||||
public static int[] visualLayout(ServerPlayer player) {
|
||||
int[] layout = SESSION_LAYOUTS.get(player.getUUID());
|
||||
return layout == null ? identityLayout() : layout.clone();
|
||||
}
|
||||
|
||||
private static int indexOf(int[] layout, int visualRow) {
|
||||
for (int index = 0; index < layout.length; index++) {
|
||||
if (layout[index] == visualRow) return index;
|
||||
|
||||
+21
@@ -20,6 +20,27 @@ public final class SanctuaryInventoryCapacity {
|
||||
private SanctuaryInventoryCapacity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a visual row (0 = live hotbar) into a top-to-bottom bank index.
|
||||
* Storage occupies the first lines; the hotbar sits on the last one, like vanilla.
|
||||
*/
|
||||
public static int bankIndex(int visualRow, int visibleRows) {
|
||||
visibleRows = Math.clamp(visibleRows, 1, MAX_ROW_COUNT);
|
||||
visualRow = Math.clamp(visualRow, 0, visibleRows - 1);
|
||||
return visibleRows - 1 - visualRow;
|
||||
}
|
||||
|
||||
public static int physicalRowAtBankIndex(int bankIndex, int[] visualByPhysical, int unlockedRows) {
|
||||
unlockedRows = Math.clamp(unlockedRows, 1, MAX_ROW_COUNT);
|
||||
bankIndex = Math.clamp(bankIndex, 0, unlockedRows - 1);
|
||||
for (int physical = 0; physical < unlockedRows; physical++) {
|
||||
int visual = visualByPhysical == null || physical >= visualByPhysical.length
|
||||
? physical : visualByPhysical[physical];
|
||||
if (bankIndex(visual, unlockedRows) == bankIndex) return physical;
|
||||
}
|
||||
return bankIndex;
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
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.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Server-authoritative sort of every unlocked inventory row except the live hotbar.
|
||||
* Armor, off-hand, crafting, cape and companion slots stay untouched.
|
||||
*/
|
||||
public final class SanctuaryInventorySort {
|
||||
private static final int COOLDOWN_TICKS = 8;
|
||||
private static final Map<UUID, Long> LAST_SORT_TICK = new HashMap<>();
|
||||
private static final SanctuaryInventorySorter.StackAccess<ItemStack> STACKS =
|
||||
new SanctuaryInventorySorter.StackAccess<>() {
|
||||
@Override
|
||||
public boolean isEmpty(ItemStack stack) {
|
||||
return stack == null || stack.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count(ItemStack stack) {
|
||||
return stack.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxStackSize(ItemStack stack) {
|
||||
return stack.getMaxStackSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sameItem(ItemStack first, ItemStack second) {
|
||||
return ItemStack.isSameItemSameComponents(first, second);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack copy(ItemStack stack) {
|
||||
return stack.copy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack withCount(ItemStack stack, int count) {
|
||||
if (count <= 0) return ItemStack.EMPTY;
|
||||
ItemStack copy = stack.copy();
|
||||
copy.setCount(count);
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack empty() {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(ItemStack first, ItemStack second) {
|
||||
boolean firstEmpty = first.isEmpty();
|
||||
boolean secondEmpty = second.isEmpty();
|
||||
if (firstEmpty || secondEmpty) return Boolean.compare(firstEmpty, secondEmpty);
|
||||
int byId = BuiltInRegistries.ITEM.getKey(first.getItem()).toString()
|
||||
.compareTo(BuiltInRegistries.ITEM.getKey(second.getItem()).toString());
|
||||
if (byId != 0) return byId;
|
||||
int byComponents = String.valueOf(first.getComponents())
|
||||
.compareTo(String.valueOf(second.getComponents()));
|
||||
if (byComponents != 0) return byComponents;
|
||||
return Integer.compare(second.getCount(), first.getCount());
|
||||
}
|
||||
};
|
||||
|
||||
private static boolean initialized;
|
||||
|
||||
private SanctuaryInventorySort() {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayerEvents.LEAVE.register(player -> LAST_SORT_TICK.remove(player.getUUID()));
|
||||
}
|
||||
|
||||
public static void sort(ServerPlayer player) {
|
||||
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);
|
||||
|
||||
int unlockedRows = SanctuaryInventoryCapacity.unlockedRows(player);
|
||||
if (unlockedRows <= 1) return;
|
||||
|
||||
Inventory inventory = player.getInventory();
|
||||
SanctuaryOverflowContainer overflow = SanctuaryOverflowAccess.of(player);
|
||||
int[] visualByPhysical = SanctuaryHotbarRows.visualLayout(player);
|
||||
List<ItemStack> slots = new ArrayList<>(
|
||||
(unlockedRows - 1) * SanctuaryInventoryCapacity.ROW_SIZE);
|
||||
for (int bank = 0; bank < unlockedRows; bank++) {
|
||||
int row = SanctuaryInventoryCapacity.physicalRowAtBankIndex(
|
||||
bank, visualByPhysical, unlockedRows);
|
||||
if (row == 0) continue;
|
||||
for (int column = 0; column < SanctuaryInventoryCapacity.ROW_SIZE; column++) {
|
||||
slots.add(getRowItem(inventory, overflow, row, column).copy());
|
||||
}
|
||||
}
|
||||
SanctuaryInventorySorter.compactAndSort(slots, STACKS);
|
||||
int index = 0;
|
||||
for (int bank = 0; bank < unlockedRows; bank++) {
|
||||
int row = SanctuaryInventoryCapacity.physicalRowAtBankIndex(
|
||||
bank, visualByPhysical, unlockedRows);
|
||||
if (row == 0) continue;
|
||||
for (int column = 0; column < SanctuaryInventoryCapacity.ROW_SIZE; column++) {
|
||||
setRowItem(inventory, overflow, row, column, slots.get(index++));
|
||||
}
|
||||
}
|
||||
inventory.setChanged();
|
||||
overflow.setChanged();
|
||||
player.inventoryMenu.broadcastChanges();
|
||||
if (player.containerMenu != player.inventoryMenu) player.containerMenu.broadcastChanges();
|
||||
}
|
||||
|
||||
private static ItemStack getRowItem(Inventory inventory, Container overflow,
|
||||
int logicalRow, int column) {
|
||||
if (logicalRow < SanctuaryInventoryCapacity.VANILLA_ROW_COUNT) {
|
||||
return inventory.getItem(logicalRow * SanctuaryInventoryCapacity.ROW_SIZE + column);
|
||||
}
|
||||
return overflow.getItem((logicalRow - SanctuaryInventoryCapacity.VANILLA_ROW_COUNT)
|
||||
* SanctuaryInventoryCapacity.ROW_SIZE + column);
|
||||
}
|
||||
|
||||
private static void setRowItem(Inventory inventory, Container overflow,
|
||||
int logicalRow, int column, ItemStack stack) {
|
||||
if (logicalRow < SanctuaryInventoryCapacity.VANILLA_ROW_COUNT) {
|
||||
inventory.setItem(logicalRow * SanctuaryInventoryCapacity.ROW_SIZE + column, stack);
|
||||
return;
|
||||
}
|
||||
overflow.setItem((logicalRow - SanctuaryInventoryCapacity.VANILLA_ROW_COUNT)
|
||||
* SanctuaryInventoryCapacity.ROW_SIZE + column, stack);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Merges identical stacks then orders them, leaving empty slots at the end.
|
||||
* The access adapter keeps this independent of Minecraft types so smoke tests
|
||||
* can cover the contract without a running game.
|
||||
*/
|
||||
public final class SanctuaryInventorySorter {
|
||||
private SanctuaryInventorySorter() {
|
||||
}
|
||||
|
||||
public interface StackAccess<T> {
|
||||
boolean isEmpty(T stack);
|
||||
|
||||
int count(T stack);
|
||||
|
||||
int maxStackSize(T stack);
|
||||
|
||||
boolean sameItem(T first, T second);
|
||||
|
||||
T copy(T stack);
|
||||
|
||||
T withCount(T stack, int count);
|
||||
|
||||
T empty();
|
||||
|
||||
int compare(T first, T second);
|
||||
}
|
||||
|
||||
public static <T> void compactAndSort(List<T> slots, StackAccess<T> access) {
|
||||
List<T> merged = new ArrayList<>();
|
||||
for (T slot : slots) {
|
||||
if (access.isEmpty(slot)) continue;
|
||||
T remaining = access.copy(slot);
|
||||
for (int index = 0; index < merged.size() && !access.isEmpty(remaining); index++) {
|
||||
T existing = merged.get(index);
|
||||
if (!access.sameItem(existing, remaining)) continue;
|
||||
int space = access.maxStackSize(existing) - access.count(existing);
|
||||
if (space <= 0) continue;
|
||||
int moved = Math.min(space, access.count(remaining));
|
||||
merged.set(index, access.withCount(existing, access.count(existing) + moved));
|
||||
remaining = access.withCount(remaining, access.count(remaining) - moved);
|
||||
}
|
||||
if (!access.isEmpty(remaining)) merged.add(remaining);
|
||||
}
|
||||
merged.sort(access::compare);
|
||||
for (int index = 0; index < slots.size(); index++) {
|
||||
slots.set(index, index < merged.size() ? merged.get(index) : access.empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -1,7 +1,9 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryInventoryClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryInventorySortClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryHotbarRowsClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryContainerScreenGeometry;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryExtendedInventoryMenu;
|
||||
@@ -191,7 +193,7 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
sanctuary$panelLeft + PANEL_BANK_X - 3
|
||||
+ minecraft.player.getInventory().getSelectedSlot() * BANK_ROW_STEP,
|
||||
sanctuary$panelTop + PANEL_BANK_Y - 3
|
||||
+ SanctuaryHotbarRowsClient.activeVisualRow() * BANK_ROW_STEP, 22, 21);
|
||||
+ SanctuaryHotbarRowsClient.bankIndex(0, sanctuary$visibleRows) * BANK_ROW_STEP, 22, 21);
|
||||
}
|
||||
graphics.text(minecraft.font,
|
||||
Component.translatable("screen.sanctuary.inventory.banks",
|
||||
@@ -245,6 +247,15 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
|
||||
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$sortInventory(MouseButtonEvent event, boolean doubleClick,
|
||||
CallbackInfoReturnable<Boolean> cir) {
|
||||
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
|
||||
if (SanctuaryInventorySortClient.request(Minecraft.getInstance())) {
|
||||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
/** Shift + held left click quick-moves every slot crossed exactly once. */
|
||||
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$startRapidTransfer(MouseButtonEvent event, boolean doubleClick,
|
||||
@@ -373,7 +384,8 @@ abstract class AbstractContainerScreenMixin implements SanctuaryContainerScreenG
|
||||
}
|
||||
sanctuary$move(slot,
|
||||
sanctuary$panelLeft + PANEL_BANK_X + column * BANK_ROW_STEP,
|
||||
sanctuary$panelTop + PANEL_BANK_Y + visualRow * BANK_ROW_STEP);
|
||||
sanctuary$panelTop + PANEL_BANK_Y
|
||||
+ SanctuaryHotbarRowsClient.bankIndex(logicalRow, sanctuary$visibleRows) * BANK_ROW_STEP);
|
||||
}
|
||||
|
||||
@Unique
|
||||
|
||||
+5
-4
@@ -163,15 +163,15 @@ abstract class InventoryScreenMixin {
|
||||
}
|
||||
}
|
||||
if (minecraft.player != null) {
|
||||
int activeRow = SanctuaryHotbarRowsClient.activeVisualRow();
|
||||
int hotbarRow = SanctuaryHotbarRowsClient.bankIndex(0, sanctuary$visibleRows);
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, HOTBAR_TEXTURE,
|
||||
leftPos + HOTBAR_X,
|
||||
topPos + BANK_Y + activeRow * BANK_ROW_STEP - 3,
|
||||
topPos + BANK_Y + hotbarRow * BANK_ROW_STEP - 3,
|
||||
0.0F, 0.0F, 182, 22, 182, 22);
|
||||
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_SELECTION_SPRITE,
|
||||
leftPos + HOTBAR_X - 1
|
||||
+ minecraft.player.getInventory().getSelectedSlot() * HOTBAR_COLUMN_STEP,
|
||||
topPos + BANK_Y + activeRow * BANK_ROW_STEP - 4, 24, 23);
|
||||
topPos + BANK_Y + hotbarRow * BANK_ROW_STEP - 4, 24, 23);
|
||||
}
|
||||
if (minecraft.player != null) {
|
||||
InventoryScreen.extractEntityInInventoryFollowsMouse(
|
||||
@@ -212,7 +212,8 @@ abstract class InventoryScreenMixin {
|
||||
return;
|
||||
}
|
||||
int x = hotbar ? HOTBAR_SLOT_X + column * HOTBAR_COLUMN_STEP : BANK_X + column * 18;
|
||||
move(slot, x, BANK_Y + visualRow * BANK_ROW_STEP);
|
||||
move(slot, x, BANK_Y + SanctuaryHotbarRowsClient.bankIndex(logicalRow, sanctuary$visibleRows)
|
||||
* BANK_ROW_STEP);
|
||||
}
|
||||
|
||||
@Unique
|
||||
|
||||
+14
@@ -2,6 +2,7 @@ package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryHotbarRowsClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryInventorySortClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuarySittingClient;
|
||||
import net.minecraft.client.KeyboardHandler;
|
||||
import net.minecraft.client.Minecraft;
|
||||
@@ -45,4 +46,17 @@ public abstract class KeyboardHandlerMixin {
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "keyPress", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$sortInventory(long windowPointer, int action, KeyEvent event,
|
||||
CallbackInfo callback) {
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (windowPointer != client.getWindow().handle() || action != GLFW.GLFW_PRESS
|
||||
|| !SanctuaryClient.isSortInventoryKey(event)) {
|
||||
return;
|
||||
}
|
||||
if (SanctuaryInventorySortClient.requestFromKeyboard(client)) {
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -1,8 +1,14 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.mixin.client;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryBulkActionsClient;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryClient;
|
||||
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.input.MouseButtonEvent;
|
||||
import net.minecraft.client.input.MouseButtonInfo;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
@@ -19,4 +25,17 @@ public abstract class MouseHandlerMixin {
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "onButton", at = @At("HEAD"), cancellable = true)
|
||||
private void sanctuary$sortInventory(long windowPointer, MouseButtonInfo buttonInfo, int action,
|
||||
CallbackInfo callback) {
|
||||
if (action != GLFW.GLFW_PRESS) return;
|
||||
Minecraft client = Minecraft.getInstance();
|
||||
if (windowPointer != client.getWindow().handle()) return;
|
||||
MouseButtonEvent event = new MouseButtonEvent(0.0, 0.0, buttonInfo);
|
||||
if (!SanctuaryClient.isSortInventoryMouse(event)) return;
|
||||
if (SanctuaryInventorySortClient.request(client)) {
|
||||
callback.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -6,6 +6,7 @@ import fr.koka99cab.sanctuary26.sanctuary.progression.SanctuaryMealHeartsService
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.building.SanctuaryBuildingManager;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.mining.SanctuaryMiningManager;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryHotbarRows;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryInventorySort;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuaryCompanionPowers;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.gameplay.SanctuarySitting;
|
||||
import fr.koka99cab.sanctuary26.sanctuary.celestial.CelestialSkyService;
|
||||
@@ -48,6 +49,7 @@ public final class SanctuaryNetworking {
|
||||
PayloadTypeRegistry.serverboundPlay().register(BuildControlPayload.ID, BuildControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(MiningControlPayload.ID, MiningControlPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(HotbarRowSwitchPayload.ID, HotbarRowSwitchPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(SortInventoryPayload.ID, SortInventoryPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(CompanionPowerUsePayload.ID, CompanionPowerUsePayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(SitRequestPayload.ID, SitRequestPayload.CODEC);
|
||||
PayloadTypeRegistry.serverboundPlay().register(CelestialActionPayload.ID, CelestialActionPayload.CODEC);
|
||||
@@ -72,6 +74,8 @@ public final class SanctuaryNetworking {
|
||||
new HotbarRowSyncPayload(accepted, payload.logicalRow()));
|
||||
}
|
||||
}));
|
||||
ServerPlayNetworking.registerGlobalReceiver(SortInventoryPayload.ID, (payload, context) ->
|
||||
context.server().execute(() -> SanctuaryInventorySort.sort(context.player())));
|
||||
ServerPlayNetworking.registerGlobalReceiver(CompanionPowerUsePayload.ID, (payload, context) ->
|
||||
context.server().execute(() -> SanctuaryCompanionPowers.use(context.player())));
|
||||
ServerPlayNetworking.registerGlobalReceiver(SitRequestPayload.ID, (payload, context) ->
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.network;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.SanctuaryMod;
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
|
||||
private void write(RegistryFriendlyByteBuf buffer) {
|
||||
// Intentionally empty: no client-supplied slot list or item identity can be trusted.
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<? extends CustomPacketPayload> type() {
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,7 @@
|
||||
"key.sanctuary.companion_power": "Companion power",
|
||||
"key.sanctuary.zoom": "Telephoto zoom",
|
||||
"key.sanctuary.switch_hotbar_row": "Cycle hotbar row",
|
||||
"key.sanctuary.sort_inventory": "Sort inventory",
|
||||
"key.sanctuary.quest_tracker": "Quest tracker",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Quest tracker shown",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Quest tracker hidden",
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
"key.sanctuary.companion_power": "Pouvoir du familier",
|
||||
"key.sanctuary.zoom": "Zoom téléobjectif",
|
||||
"key.sanctuary.switch_hotbar_row": "Changer de rangée de hotbar",
|
||||
"key.sanctuary.sort_inventory": "Trier l'inventaire",
|
||||
"key.sanctuary.quest_tracker": "Suivi des quêtes",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Suivi des quêtes affiché",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Suivi des quêtes masqué",
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
"key.sanctuary.companion_power": "Способность спутника",
|
||||
"key.sanctuary.zoom": "Телефото-зум",
|
||||
"key.sanctuary.switch_hotbar_row": "Сменить ряд панели быстрого доступа",
|
||||
"key.sanctuary.sort_inventory": "Сортировать инвентарь",
|
||||
"key.sanctuary.quest_tracker": "Трекер заданий",
|
||||
"hud.sanctuary.quest_tracker.toggle_on": "Трекер заданий показан",
|
||||
"hud.sanctuary.quest_tracker.toggle_off": "Трекер заданий скрыт",
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Executable coverage for merging, ordering and leaving the hotbar row out of the sort. */
|
||||
public final class SanctuaryInventorySortSmoke {
|
||||
private SanctuaryInventorySortSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
require(SanctuaryInventoryCapacity.bankIndex(0, 4) == 3,
|
||||
"The live hotbar must sit on the last bank row");
|
||||
require(SanctuaryInventoryCapacity.bankIndex(3, 4) == 0,
|
||||
"The last storage row must sit on the first bank line");
|
||||
require(SanctuaryInventoryCapacity.physicalRowAtBankIndex(0, null, 4) == 3,
|
||||
"Packing must start at the top storage row, not the hotbar");
|
||||
require(SanctuaryInventoryCapacity.physicalRowAtBankIndex(3, null, 4) == 0,
|
||||
"The bottom bank row must remain the live hotbar");
|
||||
|
||||
SanctuaryInventorySorter.StackAccess<FakeStack> access = FakeStack.ACCESS;
|
||||
List<FakeStack> storage = new ArrayList<>(List.of(
|
||||
new FakeStack("minecraft:dirt", "", 32, 64),
|
||||
new FakeStack("minecraft:cobblestone", "", 16, 64),
|
||||
FakeStack.EMPTY,
|
||||
new FakeStack("minecraft:dirt", "", 40, 64),
|
||||
new FakeStack("minecraft:diamond_sword", "sharpness", 1, 1),
|
||||
new FakeStack("minecraft:cobblestone", "", 64, 64),
|
||||
new FakeStack("minecraft:diamond_sword", "", 1, 1)
|
||||
));
|
||||
SanctuaryInventorySorter.compactAndSort(storage, access);
|
||||
require(storage.get(0).id.equals("minecraft:cobblestone") && storage.get(0).count == 64,
|
||||
"Full cobblestone stack must come first");
|
||||
require(storage.get(1).id.equals("minecraft:cobblestone") && storage.get(1).count == 16,
|
||||
"Leftover cobblestone must follow the merged full stack");
|
||||
require(storage.get(2).id.equals("minecraft:diamond_sword") && storage.get(2).components.isEmpty(),
|
||||
"Unenchanted sword must sort before the enchanted one");
|
||||
require(storage.get(3).id.equals("minecraft:diamond_sword") && storage.get(3).components.equals("sharpness"),
|
||||
"Component variants must stay distinct");
|
||||
require(storage.get(4).id.equals("minecraft:dirt") && storage.get(4).count == 64,
|
||||
"Dirt must merge up to max stack size");
|
||||
require(storage.get(5).id.equals("minecraft:dirt") && storage.get(5).count == 8,
|
||||
"Dirt remainder must keep the leftover count");
|
||||
require(storage.get(6).isEmpty(), "Unused storage slots must end empty");
|
||||
|
||||
List<FakeStack> hotbar = new ArrayList<>(List.of(
|
||||
new FakeStack("minecraft:torch", "", 8, 64),
|
||||
new FakeStack("minecraft:bread", "", 3, 64)
|
||||
));
|
||||
List<FakeStack> snapshot = List.copyOf(hotbar);
|
||||
require(hotbar.equals(snapshot), "Hotbar snapshot must remain untouched by storage sorting");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
private record FakeStack(String id, String components, int count, int maxSize) {
|
||||
static final FakeStack EMPTY = new FakeStack("", "", 0, 64);
|
||||
static final SanctuaryInventorySorter.StackAccess<FakeStack> ACCESS =
|
||||
new SanctuaryInventorySorter.StackAccess<>() {
|
||||
@Override
|
||||
public boolean isEmpty(FakeStack stack) {
|
||||
return stack == null || stack.count <= 0 || stack.id.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int count(FakeStack stack) {
|
||||
return stack.count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int maxStackSize(FakeStack stack) {
|
||||
return stack.maxSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sameItem(FakeStack first, FakeStack second) {
|
||||
return first.id.equals(second.id) && first.components.equals(second.components);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FakeStack copy(FakeStack stack) {
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FakeStack withCount(FakeStack stack, int count) {
|
||||
if (count <= 0) return EMPTY;
|
||||
return new FakeStack(stack.id, stack.components, count, stack.maxSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FakeStack empty() {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(FakeStack first, FakeStack second) {
|
||||
boolean firstEmpty = isEmpty(first);
|
||||
boolean secondEmpty = isEmpty(second);
|
||||
if (firstEmpty || secondEmpty) return Boolean.compare(firstEmpty, secondEmpty);
|
||||
int byId = first.id.compareTo(second.id);
|
||||
if (byId != 0) return byId;
|
||||
int byComponents = first.components.compareTo(second.components);
|
||||
if (byComponents != 0) return byComponents;
|
||||
return Integer.compare(second.count, first.count);
|
||||
}
|
||||
};
|
||||
|
||||
boolean isEmpty() {
|
||||
return ACCESS.isEmpty(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user