Compare commits

...
Author SHA1 Message Date
ChrisM-PekandCursor 7387096d98 chore(sanctuary): aligner la verif alpha.223 sur le tri des conteneurs
Verification 26.2 / check (pull_request) Canceled after 0s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 05:54:43 +02:00
ChrisM-PekandCursor 457f91b92e fix(sanctuary): trier coffres et barrels et garder la hotbar en bas
Le clic molette range aussi les conteneurs de stockage, et la hotbar reste sur la derniere ligne a la connexion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 05:52:17 +02:00
koka 6ee5cdc2fb Merge pull request 'release: tester les dernières PR dans Packwiz alpha.223' (#117) from release/26.2.0-alpha.223 into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: koka/sanctuary#117
2026-08-31 03:15:43 +00:00
koka 87428c5f0d release: test latest PRs in Packwiz alpha.223
Verification 26.2 / check (pull_request) Canceled after 0s
2026-08-31 05:13:46 +02:00
koka 0599bf6885 Merge pull request 'feat(sanctuary): ajouter les modes du suivi de quêtes' (#107) from feature/quest-tracker-display-modes into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: koka/sanctuary#107
2026-08-31 03:02:03 +00:00
koka 624f441578 Merge pull request 'feat(sanctuary): trier le stockage sans deplacer la hotbar' (#112) from feature/inventory-sorter into main
Verification 26.2 / check (push) Canceled after 0s
Reviewed-on: koka/sanctuary#112
2026-08-31 03:01:13 +00:00
ChrisM-PekandCursor d6ca1140b5 feat(sanctuary): trier le stockage sans deplacer la hotbar
Verification 26.2 / check (pull_request) Canceled after 0s
Le clic molette range les rangées débloquées depuis le haut, fusionne les piles identiques et laisse la hotbar, l'armure et les slots spéciaux intacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 04:20:33 +02:00
31 changed files with 1029 additions and 211 deletions
+7 -3
View File
@@ -6,14 +6,18 @@ Ce journal suit les **versions du modpack**. Lorsqu'un seul module change, sa ve
- Depuis `26.2.0-alpha.6`, le pack et les modules ont des versions indépendantes. I Like To Move It et Only Fun deviennent les cinquième et sixième modules actifs dans l'alpha 46.
- Les versions ci-dessous ont été reconstituées à partir des JAR archivés, des packs locaux et du code source actuel. Les dates n'étant pas enregistrées de manière fiable, elles ne sont pas inventées.
## Non distribué
## `26.2.0-alpha.223`
Module modifié : **Sanctuary `0.0.0-alpha.125`**. Le pack public reste temporairement en `26.2.0-alpha.222` jusqu'à la publication.
Module modifié : **Sanctuary `0.0.0-alpha.125`**. Les neuf autres modules conservent leur version issue de l'alpha.222.
- ajoute un tri natif de l’inventaire par clic molette configurable, uniquement sur les rangées de stockage débloquées ;
- fusionne et range les piles depuis le haut sans déplacer la hotbar, l’armure, la main secondaire, le craft ni les emplacements spéciaux ;
- garde le tri sous autorité serveur, avec un délai de huit ticks, et l’ignore en créatif ou pendant la saisie dans un champ texte ;
- ajoute un style configurable au suivi des quêtes (`N` par défaut), entre le texte transparent et le panneau original de Chris ;
- place automatiquement le style texte sous la hauteur réelle de la colonne gauche du F3 permanent afin que les deux affichages ne se recouvrent pas ;
- nomme et ordonne les contrats suivis par niveau de récompense, rubis puis saphir puis émeraude/autres, au lieu du libellé générique « Contrat du jour » ;
- conserve les données Sanctuary v11, le protocole réseau 22, les IDs et les sauvegardes existantes.
- actualise JEI vers la bêta Fabric 26.2 `30.29.0.198` et synchronise ses empreintes entre Packwiz et Prism ;
- conserve les données Sanctuary v11, le protocole réseau 22, les IDs et les sauvegardes existantes ; aucun nouveau monde ni réinitialisation n’est nécessaire.
## `26.2.0-alpha.222`
+3 -1
View File
@@ -22,10 +22,12 @@ Licence : **GNU GPL 3.0 or Later** (`GPL-3.0-or-later`)
Les descriptions FR/EN/RU sont déclarées dans chaque `fabric.mod.json` et dans les fichiers de langue. Les images fournies servent d'icônes Fabric de leur module.
Version courante du pack distribué : **`26.2.0-alpha.222`**. Cette release distribue Red-Stoner **`0.0.0-alpha.13`** et Sanctuary **`0.0.0-alpha.124`** ; les huit autres modules conservent leur version issue de l'alpha.221.
Version courante du pack distribué : **`26.2.0-alpha.223`**. Cette release distribue Sanctuary **`0.0.0-alpha.125`** ; les neuf autres modules conservent leur version issue de l'alpha.222.
La migration [CANA-01 à CANA-09](CANAPLIA.md) porte Canaplia vers Minecraft 26.2. It's Alive **`0.0.0-alpha.35`** enregistre les seize familles, les découvertes, hybridations, résines, effets serveur et le Livre interdit personnel, partageable temporairement sur un pupitre. Ambiance **`0.0.0-alpha.14`** compose les profils visuels bornés avec ses réglages d'accessibilité, et Only Fun **`0.0.0-alpha.21`** exclut explicitement Canaplia et les raccourcis de progression du Shop.
L’alpha.223 ajoute le tri natif des rangées de stockage par clic molette sans déplacer la hotbar ni les emplacements spéciaux. Le suivi des quêtes propose désormais le texte transparent aligné sous la colonne gauche du F3 ou le panneau original de Chris, avec bascule configurable sur `N` et contrats ordonnés par récompense. JEI passe à la bêta Fabric 26.2 `30.29.0.198`.
L’alpha.222 améliore le terminal de stockage avec une recherche Unicode, des quantités compactes et des emplacements qui ne glissent plus lorsqu'une pile est vidée. Elle ajoute aussi le suivi textuel des quêtes actives en haut à gauche, masquable avec une touche configurable, ainsi que des suggestions d’IDs d’items et de mobs dans l’éditeur de demandes. JEI passe à la bêta Fabric 26.2 `30.29.0.196`.
L’alpha.221 corrige la cause réelle du disque blanc ralenti et trop grave : Ambiance décode désormais la source AAC en stéréo native à 48 kHz, puis moyenne explicitement chaque paire gauche/droite vers le flux mono positionnel de Minecraft. OpenAL reste à sa hauteur normale `1.0`, sans compensation artificielle ni changement de durée.
+321 -172
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -30,5 +30,5 @@ onlyfun_version=0.0.0-alpha.25
onlyfun_lifecycle=active
sanctuary_version=0.0.0-alpha.125
sanctuary_lifecycle=active
pack_version=26.2.0-alpha.222
pack_version=26.2.0-alpha.223
maven_group=fr.koka99cab.sanctuary26
@@ -0,0 +1,60 @@
{
"schema_version": 1,
"source": {
"pack_version": "26.2.0-alpha.222",
"modules": {
"sanctuary": "0.0.0-alpha.124"
}
},
"target": {
"pack_version": "26.2.0-alpha.223",
"modules": {
"sanctuary": "0.0.0-alpha.125"
}
},
"inventory_sort": {
"owner": "sanctuary",
"default_binding": "middle_mouse",
"unlocked_storage_rows_only": true,
"packs_from_top": true,
"merges_identical_stacks": true,
"hotbar_armor_offhand_crafting_and_special_slots_unchanged": true,
"server_authoritative": true,
"server_cooldown_ticks": 8,
"ignored_in_creative": true,
"ignored_while_typing": true
},
"quest_tracker_display": {
"owner": "sanctuary",
"default_mode": "transparent_text",
"mode_toggle_default_key": "N",
"original_chris_panel_available": true,
"transparent_text_below_f3_left_column": true,
"contracts_named_and_sorted_by_reward_tier": true,
"reward_order": [
"ruby",
"sapphire",
"emerald_or_other"
]
},
"compatibility": {
"new_world_required": false,
"save_reset_required": false,
"existing_saves_supported": true,
"registry_ids_changed": false,
"sanctuary_data_version": 11,
"sanctuary_network_protocol": 22
},
"distribution": {
"pack_release_updated": true,
"packwiz_index_refreshed": true,
"prism_modrinth_index_updated": true,
"shared_client_server_mrpack": true,
"jei": {
"source": "30.29.0.196",
"target": "30.29.0.198",
"modrinth_version_id": "67Dvn8sL",
"packwiz_and_prism_synchronized": true
}
}
}
+12 -12
View File
@@ -14,15 +14,15 @@ hash = "41f4f4c88c71d0a23446d5014f7407d55ad477441847099cad24492b032335a0"
[[files]]
file = "mods/ambiance-0.0.0-alpha.22.jar"
hash = "a6b7d98971f411b202f5679aae05c1bc93754ee9a42d10dc903413b17ba97a63"
hash = "34aa79ab54ba2506b0ffef062662917de8b29f6f3f9b8d7a920a664d01185641"
[[files]]
file = "mods/anotherworld-0.0.0-alpha.55.jar"
hash = "cac322006ad9a724bb656b400cf47d84a18e76cd1a35f4bd13c112f443d89275"
hash = "2c93c364af7f617a488a8ef5c64d584041eeb24b93e4ea727570c042bd73fe2d"
[[files]]
file = "mods/events-0.0.0-alpha.0.jar"
hash = "8d2566ceb838a486fe9d8539acd127446e51d7085dcbefa86ee3c1aea4630a6d"
hash = "6c94ba89f6daf64d7144e9ea2283a98a609a01a20b7913bd51821034ee8a26fd"
[[files]]
file = "mods/fabric-api.pw.toml"
@@ -31,7 +31,7 @@ metafile = true
[[files]]
file = "mods/iliketomoveit-0.0.0-alpha.16.jar"
hash = "94e5afefab9485c40a47342dc84f401fa126be40064832929601e781b3a03738"
hash = "f0f54b9746977b085abb74fea61e8cd230f3dda905cc337866e7950fb2ee9293"
[[files]]
file = "mods/iris.pw.toml"
@@ -40,32 +40,32 @@ metafile = true
[[files]]
file = "mods/itsalive-0.0.0-alpha.36.jar"
hash = "94899dba383aff41c39f7f25ee1a6b7998aa1e9889e7b9e9039a984e0bc0882f"
hash = "ae2201e91e4ab2eaf09f32ea41c920d460cf3cf426e71bd76be5241b7e81e1a0"
[[files]]
file = "mods/jei.pw.toml"
hash = "115d2239bc86ecdad88705dbc215d27ddb6a08d94f74fa538abb6a67a00bf71e"
hash = "cffb13bae33cd8df605c62e7fa66089428dc684e0ab973f662a8a710c45c306d"
metafile = true
[[files]]
file = "mods/masterkey-0.0.0-alpha.8.jar"
hash = "102c3da5d201452be23ab84338e023c284f4770270ca945b0c63b6a996fb2922"
hash = "60de65566b1434709cdfb4c915d5bc6a7f9019dd48ebeaa04fe474f0ec7fd3b4"
[[files]]
file = "mods/onlyfun-0.0.0-alpha.25.jar"
hash = "2676072de7dbc9b27c4fd0e50e797832356fbc903e80c5bd9a5bd55c1a5aff1b"
hash = "165ffe713b09db64484cceb6c508ac5e53b5d7e69e86ea6658c3e8ea04ee1ccf"
[[files]]
file = "mods/ouch-0.0.0-alpha.10.jar"
hash = "59815a0415c48c92acfc7c18bd2399780bab9b0b5eee69c2b35a66cb1474109b"
hash = "3f66a1b0657c40b653ec9e7a7aa92e4edf72475af7e372a49d9254743696f0a5"
[[files]]
file = "mods/redstoner-0.0.0-alpha.13.jar"
hash = "0bd3888b13c894aed806696a5e73e076c7ed985eff2c242f0df9ec64ff3fcb37"
hash = "226c000f5febd841d69420316af2c14dd4ec0ef1c774c21d5926f7120f44b7e6"
[[files]]
file = "mods/sanctuary-0.0.0-alpha.124.jar"
hash = "39a3db3d1d9876ac5c7a7135a3b524c82508d0ee2e0c5c410c6515ff8931c6fb"
file = "mods/sanctuary-0.0.0-alpha.125.jar"
hash = "f0826d9be66f6498bc0455ea8075bfbaefc97c1f6a7b0d21b225003fe71ad958"
[[files]]
file = "mods/simple-voice-chat.pw.toml"
+4 -4
View File
@@ -1,14 +1,14 @@
name = "Just Enough Items (JEI)"
filename = "jei-26.2-fabric-30.29.0.196.jar"
filename = "jei-26.2-fabric-30.29.0.198.jar"
side = "both"
pin = true
[download]
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/vvwsu2BR/jei-26.2-fabric-30.29.0.196.jar"
url = "https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
hash-format = "sha512"
hash = "c62a3e264d4b0ec398b9aa717a4832eecb8663bbcec4483dc58a9999d5e4e55ca4f5bca285a0d4a003220c698c3e87c8a3294366e085e72545e4afe84aec92c7"
hash = "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
[update]
[update.modrinth]
mod-id = "u6dRKJwZ"
version = "vvwsu2BR"
version = "67Dvn8sL"
+2 -2
View File
@@ -1,12 +1,12 @@
name = "Sanctuary 26.2"
author = "KOKA99CAB"
version = "26.2.0-alpha.222"
version = "26.2.0-alpha.223"
pack-format = "packwiz:1.1.0"
[index]
file = "index.toml"
hash-format = "sha256"
hash = "53b5bec1b113ab2b353e6a142e3dbc4617ea7d27003bcbb09b668938dd262ddd"
hash = "9e06da88d562c7a2ed5fbcd56a06779a37715d4f9cf26479ee1a664758ada599"
[versions]
fabric = "0.19.3"
+6 -6
View File
@@ -3,7 +3,7 @@
"game": "minecraft",
"name": "Sanctuary 26.2",
"summary": "Sanctuary 26.2 — progression, exploration, worldgen and multiplayer systems by KOKA99CAB.",
"versionId": "26.2.0-alpha.222",
"versionId": "26.2.0-alpha.223",
"files": [
{
"path": "mods/fabric-api-0.158.0+26.2.jar",
@@ -21,19 +21,19 @@
"fileSize": 2535765
},
{
"path": "mods/jei-26.2-fabric-30.29.0.196.jar",
"path": "mods/jei-26.2-fabric-30.29.0.198.jar",
"hashes": {
"sha1": "c2f2c6b5822a560fe9872921b908ffc2f3297d00",
"sha512": "c62a3e264d4b0ec398b9aa717a4832eecb8663bbcec4483dc58a9999d5e4e55ca4f5bca285a0d4a003220c698c3e87c8a3294366e085e72545e4afe84aec92c7"
"sha1": "652e964c71a699fdfdd96cda265ea703b13c6ef4",
"sha512": "06131c9528549840e07ddc9634e23454cd454380d7749a23d818f0efe0dacc02874e967ceb74a24ee82efb13e0fe2df4026f988a68f54f890586967e91dcaa5b"
},
"env": {
"client": "required",
"server": "required"
},
"downloads": [
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/vvwsu2BR/jei-26.2-fabric-30.29.0.196.jar"
"https://cdn.modrinth.com/data/u6dRKJwZ/versions/67Dvn8sL/jei-26.2-fabric-30.29.0.198.jar"
],
"fileSize": 1838353
"fileSize": 1839279
},
{
"path": "mods/voicechat-fabric-2.6.22+26.2.jar",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"pack_version": "26.2.0-alpha.222",
"pack_version": "26.2.0-alpha.223",
"release_channel": "alpha",
"minecraft_version": "26.2",
"fabric_loader_version": "0.19.3",
@@ -9,7 +9,7 @@
"project_id": "FgOqyAXd",
"project_slug": "sanctuary-koka99",
"project_url": "https://modrinth.com/modpack/sanctuary-koka99",
"primary_file": "sanctuary-26.2.0-alpha.222.mrpack",
"primary_file": "sanctuary-26.2.0-alpha.223.mrpack",
"shared_client_server_pack": true,
"update_owner": "packwiz-installer",
"check_on_launch": true,
+27
View File
@@ -116,6 +116,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."
@@ -306,6 +314,7 @@ tasks.register("verifySanctuary") {
tasks.named("questIdSuggestionsSmoke"),
tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
tasks.named("capeJoinSyncSmoke"),
tasks.named("inventorySortSmoke"),
tasks.named("verifyBlackMarket")
doLast {
@@ -622,6 +631,22 @@ 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'
,'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'
@@ -3432,5 +3457,7 @@ tasks.named("check") {
dependsOn tasks.named("questTrackerDisplaySmoke")
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();
@@ -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 KeyMapping questTrackerModeKey;
private static boolean zoomWasHeld;
@@ -94,6 +96,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,
@@ -233,6 +241,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)
@@ -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(),
@@ -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];
}
@@ -40,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
@@ -0,0 +1,55 @@
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;
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 {
private SanctuaryInventorySortClient() {
}
public static boolean requestFromKeyboard(Minecraft client) {
if (client.gui.screen() != null && client.gui.screen().getFocused() instanceof EditBox) {
return false;
}
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<?> screen)
|| screen instanceof CreativeModeInventoryScreen
|| !ClientPlayNetworking.canSend(SortInventoryPayload.ID)) {
return false;
}
boolean container = resolveContainerTarget(screen, underCursor);
AbstractWidget.playButtonClickSound(client.getSoundManager());
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());
}
}
@@ -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;
@@ -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;
@@ -0,0 +1,193 @@
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.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 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;
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 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;
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 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) {
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);
}
}
@@ -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());
}
}
}
@@ -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();
}
@@ -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,16 @@ 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(),
sanctuary$slotAt(event.x(), event.y()))) {
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 +385,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
@@ -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
@@ -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();
}
}
}
@@ -1,8 +1,15 @@
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.gui.screens.inventory.AbstractContainerScreen;
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 +26,20 @@ 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;
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)) {
callback.cancel();
}
}
}
@@ -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,9 @@ public final class SanctuaryNetworking {
new HotbarRowSyncPayload(accepted, payload.logicalRow()));
}
}));
ServerPlayNetworking.registerGlobalReceiver(SortInventoryPayload.ID, (payload, context) ->
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) ->
@@ -0,0 +1,30 @@
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;
/**
* 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(buffer.readBoolean());
}
private void write(RegistryFriendlyByteBuf buffer) {
buffer.writeBoolean(container);
}
@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",
"key.sanctuary.quest_tracker_mode": "Quest tracker style",
"hud.sanctuary.quest_tracker.title": "Quests",
@@ -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",
"key.sanctuary.quest_tracker_mode": "Style du suivi des quêtes",
"hud.sanctuary.quest_tracker.title": "Quêtes",
@@ -155,6 +155,7 @@
"key.sanctuary.companion_power": "Способность спутника",
"key.sanctuary.zoom": "Телефото-зум",
"key.sanctuary.switch_hotbar_row": "Сменить ряд панели быстрого доступа",
"key.sanctuary.sort_inventory": "Сортировать инвентарь",
"key.sanctuary.quest_tracker": "Трекер заданий",
"key.sanctuary.quest_tracker_mode": "Стиль трекера заданий",
"hud.sanctuary.quest_tracker.title": "Задания",
@@ -0,0 +1,130 @@
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> 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)
));
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);
}
}
}