File Details
Sophisticated Tab 0.6.0 (Forge 1.20.1)
- R
- May 17, 2026
- 72.94 KB
- 20.7K
- 1.20.1
- Forge
File Name
sophisticatedtab-0.6.0.jar
Supported Versions
- 1.20.1
Curse Maven Snippet
[1.20.1-0.6.0] - 2026-05-16
Fixed
Visual item duplication when opening a backpack tab while items sit in the vanilla 2×2 crafting grid. When
InventoryScreenis the active screen, the player'scontainerMenuis theirinventoryMenu, so vanillaServerPlayer.openMenushort-circuits its owncloseContainer()call (if (this.containerMenu != this.inventoryMenu)evaluates false).InventoryMenu.removed(player)is never invoked, soclearContainer(player, craftSlots)never returns the crafting input and the carried/cursor stack is never returned to the player inventory. The freshBackpackContainerthen opens on top of that abandoned state — the visual duplicate is one symptom, but click order in the new screen could also cause real item desync. v0.5.x sentBackpackOpenMessagedirectly from BackpackTab.openTargetScreen and BackpackTabContextMenu.openBackpack; v0.6.0 routes both through a new BackpackOpenCoordinator. The coordinator inspects the activeScreen+containerMenu; when cleanup is needed (screen instanceof InventoryScreenand either the craft slots or carried stack are non-empty) it firesLocalPlayer.closeContainer()first — vanilla close path, sendsServerboundContainerClosePacket, server runsInventoryMenu.removed(player)and returns crafting input + carried stack viaplaceItemBackInInventory— then defers one client tick (via the existingClientBootstrap.onClientTick(Phase.END)listener), re-resolves the target backpack by contents UUID (IBackpackWrapper.getContentsUuid()) so post-close inventory repacking can't open the wrong backpack, then sendsBackpackOpenMessage. Backpack-to-backpack tab switches stay on the existing fast path (server'sopenMenuauto-closes the previous container becausecontainerMenu != inventoryMenu). The fix uses only vanillaServerboundContainerClosePacketand Sophisticated Backpacks' existingBackpackOpenMessage— no new packets, mod stays fully client-side.Latent server-side desync when returning to inventory via the back-to-inventory tab. BackToInventoryTab.openTargetScreen in v0.5.x called
Minecraft.getInstance().setScreen(new InventoryScreen(player))directly while aBackpackContainerwas still the active server-side menu. The server never received a close packet, so it kept theBackpackContainer"open" while the client renderedInventoryScreen— guaranteed desync on the next click in the inventory. v0.6.0 routes this throughBackpackOpenCoordinator.openInventoryFromBackpackwhich sendsServerboundContainerClosePacket(server runsBackpackContainer.removed(player), Sophisticated Core's storage cleanup, then resetscontainerMenu = inventoryMenu) and defers thesetScreen(InventoryScreen)to the next tick to keep the close-then-open order clean.Stale "Unknown backpack" entries leaking into newly created worlds. Profile keys in v0.5.x came from ProfileResolver as
"singleplayer:" + getWorldData().getLevelName()— display name only — so two saves whose display names sanitized to the same string shared one preference bucket on disk. Creating "New World" → playing → deleting → creating "New World" again inherited the prior world'sorderedBackpacksandhiddenBackpacks; the UUIDs from the first world rendered asUnknown backpackrows in the second. Different Mojang accounts on the same machine likewise inherited each other's hide/order state. v0.6.0 replaces the bare-string key with a structured BackpackScopeKey whose serialized form isv2:sp/<level-folder>/<player-uuid>for singleplayer (folder resolved viamc.getSingleplayerServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().getFileName()— stable across rename, distinct across recreate) andv2:mp/<host:port>/<player-uuid>for multiplayer/LAN/Realm (server's connection address fromServerData.ipis alreadyhost:port). UNKNOWN scope (player not loaded, no server) is now ephemeral: BackpackTabPreferences.current returns a sharedEPHEMERAL_PROFILEsingleton whoseProfileEntry.ephemeral=trueflag makes every mutator a no-op and which is never registered inPROFILES— so transient pre-world-load mutations can't accumulate into a global garbage bucket.
Changed
tab_preferences.jsonschema bumped 1 → 2 with conservative migration. PreferencesStorage.parse now returns aParseResult(profiles, migrated)record; when the loaded file'sversionis belowBackpackTabPreferences.SCHEMA_VERSION(now2), every entry's key is relocated under alegacy/prefix (e.g.singleplayer:NewWorld→legacy/singleplayer:NewWorld) andloadAllmarks the model dirty so the new schema lands on disk via the next tick's flush. Legacy entries are preserved on disk indefinitely for archeology but never read bycurrent()— fresh worlds start clean. No data is deleted. A defensive guard also drops anyv2:unknownkeys on load (they should never have been written, but if a malformed write or manual edit creates one we won't propagate it).BackpackDescriptor.iconStackis now a defensive copy. BackpackDescriptor.from previously stored the liveItemStackreference fromPlayerInventoryProvider.runOnBackpacks's callback into the record. The tab renderer dereferences it every frame, so an inventory mutation mid-render (e.g. crafting return after the new close-then-open flow) could leak into the rendered icon.stack.copy()preserves count and NBT (dye, name, upgrades) while decoupling the icon from inventory mutations.Preferences flush on world disconnect. ClientBootstrap now subscribes to
ClientPlayerNetworkEvent.LoggingOutand callsPreferencesStorage.flushIfDirty()so mutations from the world we're leaving land before the next world's scope key takes over. The staticPROFILEScache is intentionally not cleared on logout — everycurrent()call re-resolves the scope key, so the cache is per-scope by construction once keying is correct; clearing it would force a disk re-read on re-entry to an already-visited scope.
Added
BackpackScopeKeyrecord (Kind kind, String identity, UUID playerUuid).Kind.UNKNOWNis a sentinel withisPersistent() == false. Sanitization helper is on the record itself so the resolver and the storage layer share one canonical key form.Ephemeral
ProfileEntrysentinel returned byBackpackTabPreferences.current()whenever scope is non-persistent. All mutators check the privateephemeralflag and early-return; reads return empty collections. Never registered inPROFILES, never serialized.

