promotional bannermobile promotional banner

Sophisticated Tab

A simple mod which integrates Sophisticated Backpacks and Legendary Tabs for convenience; access your backpack from the inventory with the simple click of a tab, returning to your inventory the same way.

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

Forge

implementation fg.deobf("curse.maven:sophisticated-tab-1540042:8101061")
Curse Maven does not yet support mods that have disabled 3rd party sharing

Learn more about Curse Maven

[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 InventoryScreen is the active screen, the player's containerMenu is their inventoryMenu, so vanilla ServerPlayer.openMenu short-circuits its own closeContainer() call (if (this.containerMenu != this.inventoryMenu) evaluates false). InventoryMenu.removed(player) is never invoked, so clearContainer(player, craftSlots) never returns the crafting input and the carried/cursor stack is never returned to the player inventory. The fresh BackpackContainer then 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 sent BackpackOpenMessage directly from BackpackTab.openTargetScreen and BackpackTabContextMenu.openBackpack; v0.6.0 routes both through a new BackpackOpenCoordinator. The coordinator inspects the active Screen + containerMenu; when cleanup is needed (screen instanceof InventoryScreen and either the craft slots or carried stack are non-empty) it fires LocalPlayer.closeContainer() first — vanilla close path, sends ServerboundContainerClosePacket, server runs InventoryMenu.removed(player) and returns crafting input + carried stack via placeItemBackInInventory — then defers one client tick (via the existing ClientBootstrap.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 sends BackpackOpenMessage. Backpack-to-backpack tab switches stay on the existing fast path (server's openMenu auto-closes the previous container because containerMenu != inventoryMenu). The fix uses only vanilla ServerboundContainerClosePacket and Sophisticated Backpacks' existing BackpackOpenMessage — 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 a BackpackContainer was still the active server-side menu. The server never received a close packet, so it kept the BackpackContainer "open" while the client rendered InventoryScreen — guaranteed desync on the next click in the inventory. v0.6.0 routes this through BackpackOpenCoordinator.openInventoryFromBackpack which sends ServerboundContainerClosePacket (server runs BackpackContainer.removed(player), Sophisticated Core's storage cleanup, then resets containerMenu = inventoryMenu) and defers the setScreen(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's orderedBackpacks and hiddenBackpacks; the UUIDs from the first world rendered as Unknown backpack rows 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 is v2:sp/<level-folder>/<player-uuid> for singleplayer (folder resolved via mc.getSingleplayerServer().getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize().getFileName() — stable across rename, distinct across recreate) and v2:mp/<host:port>/<player-uuid> for multiplayer/LAN/Realm (server's connection address from ServerData.ip is already host:port). UNKNOWN scope (player not loaded, no server) is now ephemeral: BackpackTabPreferences.current returns a shared EPHEMERAL_PROFILE singleton whose ProfileEntry.ephemeral=true flag makes every mutator a no-op and which is never registered in PROFILES — so transient pre-world-load mutations can't accumulate into a global garbage bucket.

Changed

  • tab_preferences.json schema bumped 1 → 2 with conservative migration. PreferencesStorage.parse now returns a ParseResult(profiles, migrated) record; when the loaded file's version is below BackpackTabPreferences.SCHEMA_VERSION (now 2), every entry's key is relocated under a legacy/ prefix (e.g. singleplayer:NewWorldlegacy/singleplayer:NewWorld) and loadAll marks 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 by current() — fresh worlds start clean. No data is deleted. A defensive guard also drops any v2:unknown keys on load (they should never have been written, but if a malformed write or manual edit creates one we won't propagate it).

  • BackpackDescriptor.iconStack is now a defensive copy. BackpackDescriptor.from previously stored the live ItemStack reference from PlayerInventoryProvider.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.LoggingOut and calls PreferencesStorage.flushIfDirty() so mutations from the world we're leaving land before the next world's scope key takes over. The static PROFILES cache is intentionally not cleared on logout — every current() 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

  • BackpackScopeKey record (Kind kind, String identity, UUID playerUuid). Kind.UNKNOWN is a sentinel with isPersistent() == false. Sanitization helper is on the record itself so the resolver and the storage layer share one canonical key form.

  • Ephemeral ProfileEntry sentinel returned by BackpackTabPreferences.current() whenever scope is non-persistent. All mutators check the private ephemeral flag and early-return; reads return empty collections. Never registered in PROFILES, never serialized.