Repository navigation
feat: client-only inventory sorting (MVP) - #1
Merged
Merged
Conversation
Same conventions as LaminaPeek: keep build/, bin/ and .xmake/ out of the repository and normalise line endings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The planner works on synthetic slot states (count, max stack size, an opaque mergeability group and an ordering key) and emits Move/Swap operations plus the expected layout. Compatible partial stacks are consolidated first (42+10 -> 52, 42+30 -> 64+8, minimal stack count), then the region is arranged by key, group and count with empty slots last. It has no Minecraft dependency; whether two stacks may merge is supplied by the caller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pressing the Sort key (default R, registered with the game's keyboard settings) sorts the main 27 player inventory slots, or the hovered ordinary storage container (chest, barrel, shulker box, ...). - ScreenTracker observes the active container screen from the UI render callback and drops it on onLeave. - StackClassifier assigns mergeability groups with the game's own ItemStackBase::isStackable(other) and orders kinds by the client's creative item registry (tab, group, entry), falling back to identifier order. - SortSession snapshots the region, runs the planner and executes each step through ContainerManagerController::handlePlaceAmount / handleSwap, i.e. the same ItemStackRequest path the inventory UI uses, verifying the touched slots against the simulation before and after every step. Any disagreement, refused transfer or held cursor item aborts the remaining steps. - ResponseLogger reports item stack requests the server rejects. - Config file (sortKeyCode, sortContainers) and a --trace=y build with an immediately flushed trace.log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also run the planner tests in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the literal Creative-tab ordering with a small semantic layer on top of the game's own Creative registry: - Sections: Shulker Boxes, Equipment, Items, Construction, Nature, unknown items, empty slots. Blocks are no longer split by tools. The section comes from the registry entry's Creative category; the only overrides are Shulker Boxes (always first) and food (Item::isFood -> Items, since Bedrock files most food under Equipment). - Shulker Boxes: filled before empty (real "Items" contents), then custom name, then a content signature built from the virtually merged and sorted contents (independent of internal slot layout), then colour; the NBT hash only breaks remaining ties. - Variants of one item: custom-named first (normalised, formatting codes stripped), then enchanted (canonical enchantment list: id order, level descending, longer list first), then plain; damage ascending so the better-conditioned item leads. - SortKey moves to its own header with a field-by-field comparator; planned-layout logging shows the section and variant of each stack. No sorting profiles or comparator configuration; the default is the product. Transaction execution, consolidation, region selection and safety verification are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Build against the 26.51.3 SDK (checked out from the upstream tag until the LeviMC xmake-repo lists it). The only API change that touches LaminaSort is ll::io::FileSink taking a RotatePolicy instead of an open mode since 26.51.2; the trace sink now passes RotatePolicy::disabled(), which keeps the previous append-only behaviour. All other SDK headers LaminaSort relies on are unchanged between 26.51.1 and 26.51.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The content signature of a Shulker Box only recorded each inner stack's section, Creative index, identifier and aux value, so a box holding an Efficiency V pickaxe, one holding a plain pickaxe and one holding a worn pickaxe all produced the same signature. Inner stacks now get the same ordering key an inventory item would get (custom name, enchantments, damage, ... but never nested contents, so the description stays bounded to one level); entries with equal keys are merged and sorted, so the signature still does not depend on the internal slot layout. Shulker Boxes are recognised by comparing the item's vtable with the exported ShulkerBoxBlockItem vtable instead of an identifier suffix match, which also covers the undyed box and excludes add-on items that merely end in "shulker_box". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacks carrying the vanilla item lock component were sorted like any other, producing transfers the server rejects and reverts. The planner now knows about fixed slots: it works on the movable slots as if they were a contiguous region and maps the operations back, so no step ever addresses a fixed slot, nothing is merged into or out of it, and the rest of the region is sorted around it. applyOperation refuses any step touching a fixed slot. The classifier reads the lock mode through ItemLockHelper: lock_in_slot is always fixed; lock_in_inventory may still change slots inside the player inventory (as vanilla allows) and is left alone inside a container region, where vanilla never lets a player put it. The session logs how many slots stayed in place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typing an "r" into the Creative search field or an anvil name box fired the Sort key handler, sorting the inventory mid-word. The key stays bound; a new TextInputTracker hooks the game's own ScreenView::_fireSelectedStateChangeEvent to know which text boxes of the current screen are selected for typing, and the handler ignores the press while any is. ScreenTracker now also remembers the ScreenView that owns the current container controller and resets the tracker's knowledge when the screen changes or is left. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tics State precisely that the server's verdict on each transfer arrives asynchronously after all steps were issued, is only logged, and is reverted by vanilla rather than fed back into the sort. Describe the item-lock behaviour, the Sort key staying quiet while typing, the class-based Shulker Box detection, the inner-item state in the content signature and the LLVM requirement for building the SDK package. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements the LaminaSort MVP: a small, client-only inventory sorter for LeviLamina Client 26.51.3 (Minecraft Bedrock 1.26.51.x). Pressing the Sort key while an inventory or container screen is open consolidates compatible partial stacks and arranges the region into a zero-config, human-oriented default order (Shulker Boxes, gear, items, blocks) built on the game's own Creative registry, using the same client inventory machinery the mouse uses.
MVP scope
R; registered through LeviLamina'sKeyRegistry, so it appears in and is remappable from the game's Keyboard & Mouse settings asLaminaSort.sort). The key is ignored while a text box (Creative search, anvil name, ...) is being edited and while an item is held on the cursor.ContainerType::Container(chest, trapped chest, ender chest, barrel, shulker box),MinecartChestandChestBoatqualify; furnace, blast furnace, smoker, brewing stand, crafting, smithing, enchanting, anvil, loom etc. are never sorted.lock_in_slotstack stays exactly where it is (never merged into or out of) and the rest of the region is sorted around it; alock_in_inventorystack may still change slots inside the player inventory (as vanilla allows) and is left alone inside a container region.Client-only architecture / boundary
The mod needs only the Minecraft client with LeviLamina. No server component, operator permission, cheat, command, experiment or direct mutation of authoritative inventory state. Every change goes through the vanilla
ItemStackRequestpath, so the server validates every step and vanilla itself reverts anything it rejects.Inventory operation path reused from vanilla
Traced from
ContainerScreenController(the screen the mouse talks to) down toContainerManagerController->_transfer()->ItemStackRequestScope->ItemStackNetManagerClient(predictive containers + server-validated request). LaminaSort calls the twoContainerManagerControllerentry points that the UI uses:handlePlaceAmount(src, count, dst)- a Place transfer ofcountitems (the path behind shift-click / drag placement). Used for every merge and every move into an empty slot.handleSwap(a, b)- the Swap transfer behind the hotbar hotkey swap. Used only for two occupied slots of different kinds.No cursor is involved, no packets are built by hand, and no container/model state is written directly.
Stack compatibility semantics
Two stacks are mergeable iff vanilla's own
ItemStackBase::isStackable(other)says so - the check the game uses when stacks are combined in the UI. Damage, enchantments, custom names, lore, components, NBT, potion variants, shulker contents,can_place_on/can_destroyrestrictions and unstackable items (max stack 1) are therefore kept apart exactly as the inventory screen keeps them. LaminaSort defines no equality of its own; the planner only sees an opaque group id.Stack consolidation
Part of the MVP. For each mergeable group the lowest-index stacks are topped up from the highest-index ones, respecting the item's own max stack size, so every group ends with the minimum number of stacks and at most one partial one:
42 + 10 -> 52,42 + 30 -> 64 + 8,42 + 10 + 25 -> 64 + 13,15 + 15 eggs -> 16 + 14. Max-stack-1 items are never merged. Locked (fixed) slots take no part in consolidation.Sort ordering (zero-config default)
A small semantic layer on top of the game's own Creative registry — no profiles, no configuration, no maintained item lists:
ShulkerBoxBlockItem, not by identifier) — filled boxes before empty ones ("filled" = the realItemscontents hold at least one non-empty stack), then custom name, then a content signature, then colour; the NBT hash only breaks remaining ties. The signature is built from the inner stacks' full ordering keys (custom name, enchantments, damage, ... but never nested contents, so it is bounded to one level), merged per kind and sorted, so two boxes with the same items in different internal slots compare equal while a box holding an Efficiency V pickaxe, one with a plain pickaxe and one with a worn pickaxe are all distinct.Item::isFood; Bedrock files most food under Equipment/Nature, this is the one category override besides Shulker Boxes).Inside a section: the Creative registry group and entry order. Variants of one base item (already separated by vanilla stackability): custom-named first (name normalised: formatting codes stripped, case-folded), then enchanted (canonical list in enchantment registry id order, higher level first, longer list first — identity before any summed "power"), then plain; then damage ascending (better condition first); then the last-resort hash. Within otherwise equal stacks: count descending (
64, 64, 12).Design references reviewed (behaviour only, no code copied): Quark
SortingHandler(class-based types, blocks last, tier → enchantment power → damage), Inventory Profiles Next default rules (custom_name→ creative order → enchantments in tooltip order → damage → components), Mouse Wheelie (creative order; count → custom-named first → tooltip lines). Named-first and better-durability-first are the common conventions; IPN's global named-first was considered and rejected in favour of named-first within the item kind, which reads better in a chest.Protected / excluded slots
Hotbar, armour, offhand, crafting input/output, cursor and every other collection are never addressed: the planner only ever sees the selected region (
inventory_itemsor the container's own collection) and every operation's source and destination lie inside it. Item-locked slots inside the region are cut out of the plan (SlotStack::fixed()): the planner sorts the movable slots as a contiguous view and maps operations back, andapplyOperationrefuses any step that touches a fixed slot.Safety / abort behaviour
Before each step the two touched slots are compared with the planner's simulation (count +
matchesItemagainst a snapshot of the group's representative); after each step they are compared again and the transfer's return value is checked. Any mismatch or refusal aborts the remaining steps with a logged reason; an item held on the cursor skips the sort entirely. Steps already issued are ordinary server-validated transfers, so an aborted sort is still a valid inventory.Server rejection semantics (precise): the checks above compare against the client-side prediction. The server's verdict arrives asynchronously, after the whole sequence of steps has been issued, and is not fed back into the sort session. If the server rejects a request, vanilla discards the client's prediction for that step and restores the server's slot contents, exactly as for a rejected manual drag; LaminaSort only observes
ItemStackResponsePacketand logs a warning per rejection (and, in trace builds, the accepted count). It never retries, re-plans or "fixes up" a rejected step.Automated tests
LaminaSortTests: 24 ordering tests (SortKeyTest.cpp) and 30 planner tests (SortPlannerTest.cpp), all passing.42 + 10 -> 52,42 + 30 -> 64 + 8, separated partial stacks, more than two partial stacks, many partials -> minimal stack count, already-full stacks, full + partial, max-stack-16 (incl. no overshoot), non-stackable items (incl. defensive same-group case), look-alike incompatible stacks, look-alikes with equal keys stay contiguous, empty-slot placement, determinism, idempotent re-sort, operations stay inside the region, full 27-slot region (swaps only), creative index before name, locked slot stays and others sort around it, locked slot never merged into or out of, locked slots idempotent, all slots locked is a no-op, locked empty slot is just empty,applyOperationrejects fixed slots, invalid operations rejected. Every planner test also asserts the invariants that fixed slots are untouched and group totals are preserved.These use synthetic group ids and deliberately do not claim vanilla compatibility; that is decided at runtime by
isStackable.Environment recovery and LeviLamina 26.51.3 migration (this revision)
The local LeviLamina client environment was broken when this revision started (LeviLauncher's LIP install of LeviLamina failed with
ERR_LIP_INSTALL_FAILEDwhile fetching thebedrock-runtime-datarelease asset). Root cause, with evidence from the launcher log and direct downloads: LIP 0.34.8's downloader has a 100 s HTTP client timeout, and on this connectionrelease-assets.githubusercontent.comdelivered the 40.8 MBbedrock-runtime-data-v26.51.1-client.4-windows-x64.zipat ~0.2–0.5 MB/s (80–185 s per attempt), so every attempt timed out at ~100 s. No proxy, DNS, TLS or firewall problem was found (a directcurldownload succeeded with a matching SHA-256). Recovery used normal package management: the same LIP 0.34.8 CLI, run against the existing instance, installedgithub.com/LiteLDev/LeviLamina#client@26.51.3on an attempt where the download finished just under the timeout; nothing was copied into place by hand, the instance was preserved, and LeviLauncher then reported LeviLamina 26.51.3 installed. Minecraft boots and showsv26.51/LeviLamina-26.51.3.Migration:
xmake.luatargetslevilamina v26.51.3(git-tag form until the LeviMC xmake repo lists it; the SDK is built from source, which needs LLVM 22.1.8 rather than the VS-bundled clang). Every SDK header LaminaSort depends on is byte-identical between 26.51.1 and 26.51.3 exceptll/api/io/FileSink.h, whose constructor now takes aRotatePolicy; the--trace=ybuild usesRotatePolicy::disabled(). README/CHANGELOG updated accordingly.LaminaPeek compatibility (reported only, repository not modified): the sibling LaminaPeek mod (built for 26.51.1) loads and enables alongside LaminaSort on LeviLamina 26.51.3 without any change; it does not use
FileSink, so no source change is needed there.Local Minecraft runtime validation performed by Claude
All in a fresh, disposable local creative world ("LaminaSort Test") on the existing LeviLauncher instance. Items were placed with
/replaceitem//setblock(cheats enabled on the test world only for setup; the mod itself uses no commands). Claude connected to no multiplayer server, Realm, BDS, LAN world or remote host at any point; all of Claude's testing was in local single-player worlds. (A "trust this server?" prompt for a LAN-advertised world appeared once in the world list during the first task and was declined.)This revision, on LeviLamina 26.51.3 (trace build for the detailed checks, then the final release build):
r/Rinto the Creative search box and into the anvil name box no longer triggers a sort (logged "Sort key pressed while a text box is being edited; ignored"); after deselecting the box the key sorts again.lock_in_slotoak log in the player inventory stayed in its slot while the rest sorted around it (logged "1 item-locked slot(s) stay in place"); alock_in_inventoryapple moved within the inventory. Re-checked on the release build: 14 operations applied, all accepted, second press "already sorted".42 + 10 → 52(diorite) and42 + 30 → 64 + 8(tuff).container_items, so the region selection covers it.Earlier revisions (LeviLamina 26.51.1, same local world): empty inventory no-op; scrambled 27-slot region sorted into creative order with hotbar/armour/shield untouched;
4 × 20 dirt → 64 + 16,15 + 15 + 2 eggs → 16 + 16,10 + 9 + 3 eggs → 16 + 6, full64 + 64untouched,64 + 1kept; two swords / damaged vs undamaged pickaxe stay separate;can_place_oncobblestone merged only among itself (1 + 19 + 20 → 40) and never with plain (vanilla behaviour demonstrated by hand first); idempotency (second press 0 operations, three rapid presses no change); every issued request acknowledged (19/19, 13/13, 11/11, 7/7, 5/5, 3/3, 1/1, 40/40, 32/32); chest vs inventory region selection by hovered slot; 54-slot chest with the full section layout (filled boxes → named empty box → empty boxes; helmet, chestplate, swords, "Old Faithful", Efficiency+Unbreaking, Efficiency V, Fortune III, plain, damaged 600/1400 pickaxes, bow, arrows, shield; golden apple, bread, torch, coal, iron, string; planks 64+6, glass, cobblestone, dirt, stone, sand, logs 64+20, seeds); contents changed manually / by command between sorts; screens closed/reopened between sorts. No ghost, missing or duplicated items, no reverts, no UI breakage, no crash observed.Owner-reported manual validation (not performed by Claude)
Remaining validation (not yet exercised in game by anyone, as far as this PR knows)
lock_in_slotitem inside a container region (only the player-inventory case was exercised; the planner treats both the same).isStackablesemantics and unit tests, not exercised at runtime).Version-sensitive hooks / symbols
No signatures or fixed offsets. Everything goes through the LeviLamina 26.51.3 SDK headers:
ContainerScreenController::$onLeave,ScreenView::_fireSelectedStateChangeEvent(TextEditComponent const&, bool)(text-box focus),ClientNetworkHandler::$handle(NetworkIdentifier const&, ItemStackResponsePacket const&)(diagnostic only).ContainerManagerController::{handlePlaceAmount, handleSwap, getItemStack, getContainerSize, hasContainerController, getContainerType},ContainerScreenController::_isCursorSelectedActive,ItemStackBase::{isStackable, matchesItem, getMaxStackSize, getCustomName, getDamageValue, ...},ItemLockHelper::getItemLockMode,ShulkerBoxBlockItem::$vftable()(class identity),Item::isFood()(virtual),ItemStack::fromTag,Level::getItemRegistry().getCreativeItemRegistry().TypedStorage:ContainerScreenController::{mContainerManagerController, mHoveredSlotData},ScreenView::mController,CreativeItemRegistry::{mCreativeItems, mCreativeGroups},CreativeItemEntry::{mGroupIndex, mIndex, mItemInstance},CreativeGroupInfo::mCategory,ItemStackResponseInfo.ench(id/lvl) andItems(container contents);Enchant::Typeid order.inventory_items,container_items,barrel_items,shulker_box_itemsare used only after the screen confirms such a collection exists (in practice chests and shulker boxes both reportcontainer_items).Follow-ups intentionally left out
Move matching / move all, hotbar-inclusive sorting, LaminaSort's own slot locking (vanilla item locks are honoured), auto-restock, background sorting, inventory search/highlighting, custom sort profiles, drop-all, an in-screen Sort button (a remappable key was chosen so the sorting core does not wait on UI injection), and any server-side component.
Which issues does this PR resolve?
None filed.
Checklist before merging
🤖 Generated with Claude Code