
BackPackSSS
Custom tiered backpacks with progression crafting, upgrade system and persistent storage — designed for survival servers.
Список изменений
[1.9] - 2026-05-24
Fixed
-
🔴 Critical — Race condition: async save after connection close
saveSession()dispatches asynchronous write tasks. If one of those tasks was still in flight whensyncSaveAll()returned andDatabaseManager.closeConnection()was called (during shutdown or/bp reload), the task would attempt to write to an already-closed connection, silently losing data. Fixed by introducing avolatile boolean shuttingDownflag inBackpackManager:syncSaveAll()sets the flag before entering, which causessaveSession()to no-op for any new attempts.syncSaveAll()then spin-waits (up to 5 seconds) for any already-running async tasks to land before proceeding with its own synchronous pass, ensuring the connection is only closed after all writes have completed. -
🔴 Critical — Memory leak:
sessionsmap never evicted after closecloseSession()removed a backpack fromactiveBackpacksandplayerToBackpackbut never calledsessions.remove(). Every unique backpack UUID opened since the last restart accumulated indefinitely, causing unbounded memory growth on long-running servers. Fixed by addingsessions.remove(backpackUUID)in bothcloseSession()andclosePlayerSession(). -
🟡 Medium —
DatabaseManager.init()could fail silently on first runcreateNewFile()throwsIOExceptionwhen its parent directory does not exist. On a fresh install, ifsaveDefaultConfig()had not yet been called, the database file could not be created, leaving the connection null and causing NPEs on any subsequent load. Fixed by callingplugin.getDataFolder().mkdirs()before attempting to create the file, and added a return-early path whencreateNewFile()returnsfalse. -
🟡 Medium —
BackpackSession.playerUUIDwas alwaysnullgetBackpackSession()always passednullas the first constructor argument, makingBackpackSession.getPlayerUUID()permanently useless. Fixed by threading thePlayer's UUID fromopenBackpack()through togetBackpackSession(), which now acceptsUUID playerUUIDas its first parameter. -
🟡 Medium —
isForbiddenItem()usedgetMaxStackSize() == 1, which was too broad The heuristic blocked music discs, name tags, saddles, empty buckets, and any item from third-party plugins that happens to have a stack size of 1 for unrelated reasons. Replaced with an explicit category check usingMaterial.name()suffixes (armor, swords, axes, pickaxes, shovels, hoes) and individual constants for bows, crossbows, tridents, shields, elytras, shears, flint-and-steel, enchanted books, and totems of undying — exactly the items documented inconfig.yml. -
🟡 Medium —
onClick()marked session dirty before restriction checkssession.setDirty(true)was called at the top ofonClick(), before the nesting and item-filter checks that might cancel the event. A cancelled click does not modify the inventory, so marking it dirty caused unnecessary async saves. Fixed by movingsetDirty(true)to execute only after all checks pass. Same fix applied toonDrag(). -
🟡 Medium —
PlayerInteractEventfired for both hands, processing the backpack twice Without anEquipmentSlot.HANDguard, right-clicking with a backpack in the main hand generated twoPlayerInteractEventcalls per click. The opening cooldown absorbed the second call, but both calls enteredonInteract()and evaluated the PDC. Addedif (event.getHand() != EquipmentSlot.HAND) return;as an early exit. -
🟡 Medium —
InventorySerializerdid not close streams on exception BothtoBase64()andfromBase64()closed theirBukkitObject*Streamonly on the happy path. If serialization or deserialization threw mid-loop, the stream leaked. Replaced both methods with try-with-resources blocks. -
🟡 Medium —
PrepareItemCraftEventiterated player inventory unnecessarilycountBackpacks()was called on everyPrepareItemCraftEventtick even whenmax-backpackswas0(unlimited). Added an early-return guard so the inventory scan only runs when a real limit is configured. -
🟢 Minor —
BackpackCommandlacked explicit tier range validation A tier number outside[1, 3]was silently forwarded tocreateBackpack(), which returnednull, producing a generic "invalid tier" message through an indirect path. Added upfrontInteger.parseInt()+ range check againstTIER_MIN/TIER_MAXconstants, returning themessages.invalid-tierconfig message immediately.

