From 82c2c3f1a15011e92023081061b0f9bc73e229b4 Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 3 Jul 2026 11:53:32 -0700 Subject: [PATCH 1/7] ci: bump pinned publish-platforms.yml to ca2dcd1 --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fbbf938..5c21311 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@fe4b1f03c19f4fd4212020a06a07a7097923adec # master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@ca2dcd167e8db4e0f671a976080744dda43801a6 # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild hangar_slug: "InvSwitcher" # blank = skip Hangar From 63ae07d4e0e18a6d46a651b0ad9d3959deda29f5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sun, 26 Jul 2026 19:47:38 -0700 Subject: [PATCH 2/7] docs: warn against running a second inventory manager Running InvSwitcher alongside Multiverse-Inventories (or PerWorldInventory, MultiInv, etc.) makes both plugins save and restore the player on every world change, so they overwrite each other's data. The symptom is disappearing items with nothing logged, and it is independent of the InvSwitcher version. Documents the Multiverse-Inventories case, including the two dead ends admins usually hit first - inventory groups do not control whether MV-I handles a world, and `/mv remove` is undone when Multiverse-Core re-registers the worlds on the next restart - plus the working bypass-permission fix. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YXnYDGiASdSUZFtNyS4jbc --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 311ee1c..71f8160 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,55 @@ The following are switched per-world: 3. Done! 4. (Optional) If you would prefer to have achievements not broadcasted in your server chat when players change worlds, run the command `/gamerule announceAdvancements false` in-game, or in your server console by removing the "/" symbol. +## Do not run two inventory managers + +InvSwitcher must be the only plugin managing per-world inventories. Running it alongside +Multiverse-Inventories, PerWorldInventory, MultiInv or similar makes both plugins save and restore +the player on every world change, and they overwrite each other's data. + +The symptom is disappearing items: pick up an item on your island, go to the lobby, come back, and +the island inventory is empty. Nothing appears in the console, because neither plugin is failing — +each is faithfully saving a player state the other has already rewritten. The InvSwitcher version +makes no difference, so if you are seeing this, look for a second inventory plugin first. + +### Multiverse-Inventories + +Two things commonly mislead admins: + +* **Leaving the BentoBox worlds out of every inventory group does not help.** Groups control which + worlds *share* an inventory, not which worlds Multiverse-Inventories handles. It still writes a + per-world profile for a world that is in no group. +* **`/mv remove ` does not help either.** Multiverse-Core re-registers BentoBox worlds as + they are created, so the removal is undone on the next restart. `auto-import-3rd-party-worlds: + false` does not prevent it — that only suppresses the import sweep run when Multiverse-Core + starts, which is before BentoBox has created its worlds. + +Multiverse-Inventories has no config option to ignore a world, but it does have a bypass +permission. Enable it in the Multiverse-Inventories `config.yml` (it ships as `false`): + +```yml +share-handling: + enable-bypass-permissions: true +``` + +Then grant `mvinv.bypass.world.` for each BentoBox world — including its nether and end — to +every player. With LuckPerms: + +``` +lp group default permission set mvinv.bypass.world.bskyblock_world true +lp group default permission set mvinv.bypass.world.bskyblock_world_nether true +lp group default permission set mvinv.bypass.world.bskyblock_world_the_end true +``` + +Setting the nodes on a group every player inherits (such as `default`) covers new players +automatically. Repeat for each game mode world you run. + +**Operators do not get this permission automatically** — it has to be granted explicitly. Testing +as an op without it looks exactly like the fix not working. + +Use one node per world. Avoid `mvinv.bypass.world.*`, which switches Multiverse-Inventories off for +every world, including the ones you still want it to manage. + ## Config.yml The config allows to define which worlds that InvSwitcher should operate, and what aspects should be kept separate. From e45931303fdfced46170e6166759dd38b035f105 Mon Sep 17 00:00:00 2001 From: tastybento Date: Mon, 27 Jul 2026 07:23:05 -0700 Subject: [PATCH 3/7] fix: add missing game mode worlds to the default config The shipped `worlds:` list had not kept up with the game modes BentoBox now has, so SkyGrid, Raft, Brix and Parkour were absent. On a fresh install InvSwitcher silently did not manage those worlds, and because nothing is logged for a world that was never hooked, there is no signal that coverage is missing until items start leaking between worlds. This surfaced on a server running Multiverse-Inventories alongside InvSwitcher. After granting `mvinv.bypass.world.parkour_world` to stop MV-I handling the world, `parkour_world` was left with no inventory manager at all - it was not in InvSwitcher's world list either - and items carried straight out of the parkour world into the lobby. World names verified against each game mode's own config: skygrid-world (SkyGrid), raft_world (Raft, from Settings.java), brix_world (Brix), parkour_world (Parkour). Existing entries are untouched, and only new installs pick up the additions. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YXnYDGiASdSUZFtNyS4jbc --- src/main/resources/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e04efdd..04844d9 100755 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -9,6 +9,10 @@ worlds: - caveblock-world - poseidon_world - stranger_world +- skygrid-world +- raft_world +- brix_world +- parkour_world options: # # Per-world settings. Gamemode means Survivial, Creative, etc. From fc966316335cd32bf88d958174c915c449ac9ab5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 29 Jul 2026 09:07:25 -0700 Subject: [PATCH 4/7] ci: bump publish-platforms pin to the multipart-JSON fix The pinned SHA predates BentoBoxWorld/.github#13, which moved the CurseForge metadata and Hangar versionUpload JSON out of inline curl -F values and into files. curl treats a ';' in an inline -F value as the start of a type= attribute, so a release body containing a semicolon truncates the JSON and both platforms reject the upload. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01SAQ3YQkcfRoCZEwa6SPJcp --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5c21311..01057d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ on: jobs: publish: - uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@ca2dcd167e8db4e0f671a976080744dda43801a6 # master + uses: bentoboxworld/.github/.github/workflows/publish-platforms.yml@1f91a0edf72e8c86d671b3b8fdd3121ac6fb88e1 # master with: use_release_asset: "true" # publish the jar attached to the release; do not rebuild hangar_slug: "InvSwitcher" # blank = skip Hangar From 74773c55275f73672cb42027a853f1fbeaad353b Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 1 Aug 2026 10:43:59 -0700 Subject: [PATCH 5/7] fix: register economy command permissions and save synchronously on shutdown Two fixes, released as 1.19.2. Economy command permissions were never declared. The commands set permissions like "invswitcher.balance", which CompositeCommand prefixes with the parent game mode's permission prefix, giving e.g. "bskyblock.invswitcher.balance". addon.yml had no permissions section, so those nodes were never registered with a default and hasPermission() was false for every non-op player - / balance and / pay failed with a permission error. Declare them in addon.yml using BentoBox's [gamemode] placeholder, which AddonsManager expands into every game mode's prefix. User commands default to true, admin eco commands to op. Shutdown saves are now synchronous. BentoBox closes its database immediately after addons are disabled and only kicks players afterwards, so the async write issued from saveOnShutdown() lost the race and was silently dropped, and the PlayerQuitEvent that would otherwise save fired after the database was closed. Everything a player did since their last world change went unsaved on server stop, and because onPlayerJoin re-applies the stored inventory, the stale snapshot then overwrote their real inventory on the next login. Route both save paths through a persist() helper that writes synchronously when shutting down. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0183f7bfWoU9rmXxRCRwvrAs --- pom.xml | 2 +- .../com/wasteofplastic/invswitcher/Store.java | 30 +++++++- src/main/resources/addon.yml | 28 +++++++- .../wasteofplastic/invswitcher/StoreTest.java | 72 +++++++++++++++++++ 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 4767d14..59f6e5b 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ -LOCAL - 1.19.1 + 1.19.2 BentoBoxWorld_addon-invSwitcher bentobox-world diff --git a/src/main/java/com/wasteofplastic/invswitcher/Store.java b/src/main/java/com/wasteofplastic/invswitcher/Store.java index a5c69f1..52b41c8 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/Store.java +++ b/src/main/java/com/wasteofplastic/invswitcher/Store.java @@ -474,10 +474,36 @@ public void storeAndSave(Player player, World world, boolean shutdown) { } if (settings.isStatistics()) { String k = settings.isIslandsStatistics() ? islandKey : worldKey; - saveStats(store, player, k, shutdown).thenAccept(database::saveObjectAsync); + // On shutdown saveStats() gathers synchronously and returns an already-completed + // future, so thenAccept runs on this thread and persist() writes before we return. + saveStats(store, player, k, shutdown).thenAccept(s -> persist(s, shutdown)); return; } - database.saveObjectAsync(store); + persist(store, shutdown); + } + + /** + * Writes the store to the database, synchronously when the server is shutting down. + *

+ * Saves are normally asynchronous, but a shutdown save must not be. BentoBox closes its + * database immediately after addons are disabled, and players are only kicked afterwards, so an + * asynchronous write issued from {@link #saveOnShutdown()} loses the race and is silently + * dropped — and the {@code PlayerQuitEvent} that would otherwise save them fires after the + * database is already closed. + *

+ * The effect was that everything a player did since their last world change went unsaved when + * the server stopped. Because {@code PlayerListener.onPlayerJoin} re-applies the stored + * inventory on login, the stale snapshot then overwrote the player's real inventory and they + * were rolled back to their last world change. + * @param store - the store to write + * @param shutdown - true if this is a shutdown save, which must be synchronous + */ + private void persist(InventoryStorage store, boolean shutdown) { + if (shutdown) { + database.saveObject(store); + } else { + database.saveObjectAsync(store); + } } private CompletableFuture saveStats(InventoryStorage store, Player player, String worldName, diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml index 5cfe563..e681a54 100755 --- a/src/main/resources/addon.yml +++ b/src/main/resources/addon.yml @@ -5,4 +5,30 @@ api-version: 3.17.0 authors: tastybento -softdepend: AcidIsland, BSkyBlock, SkyGrid, CaveBock, AOneBlock \ No newline at end of file +softdepend: AcidIsland, BSkyBlock, SkyGrid, CaveBock, AOneBlock + +# Economy command permissions. These are only usable if options.money is enabled and Vault is +# installed - InvSwitcher does not register the commands otherwise. The [gamemode] placeholder is +# expanded by BentoBox into every game mode's permission prefix, e.g. bskyblock.invswitcher.balance +permissions: + '[gamemode].invswitcher.balance': + description: Player can use the balance command + default: true + '[gamemode].invswitcher.pay': + description: Player can use the pay command + default: true + '[gamemode].invswitcher.admin.eco': + description: Player can use the admin eco command + default: op + '[gamemode].invswitcher.admin.eco.balance': + description: Player can use the admin eco balance command + default: op + '[gamemode].invswitcher.admin.eco.give': + description: Player can use the admin eco give command + default: op + '[gamemode].invswitcher.admin.eco.take': + description: Player can use the admin eco take command + default: op + '[gamemode].invswitcher.admin.eco.set': + description: Player can use the admin eco set command + default: op diff --git a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java index b46eea3..de7b57b 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java @@ -18,6 +18,7 @@ import static org.mockito.Mockito.when; import java.io.File; +import java.lang.reflect.Field; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -54,6 +55,7 @@ import org.mockito.quality.Strictness; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.database.Database; import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; import com.wasteofplastic.invswitcher.dataobjects.InventoryStorage; @@ -905,4 +907,74 @@ void testGetStorageKeyForEventMultipleIslandsNotOwner() { } } + // --- Shutdown save tests --- + + /** + * Replaces the Store's database with a mock so the save path can be observed. + */ + @SuppressWarnings("unchecked") + private Database injectMockDatabase() throws Exception { + Database db = mock(Database.class); + Field field = Store.class.getDeclaredField("database"); + field.setAccessible(true); + field.set(s, db); + return db; + } + + /** + * A shutdown save must be synchronous. BentoBox closes its database immediately after addons + * are disabled, and players are only kicked afterwards, so an asynchronous write issued from + * saveOnShutdown() loses the race and is silently dropped. Everything the player did since + * their last world change was then lost, and because onPlayerJoin re-applies the stored + * inventory on login, the stale snapshot overwrote their real inventory on the next restart. + */ + @Test + void testShutdownSaveIsSynchronous() throws Exception { + sets.setStatistics(false); + sets.setAdvancements(false); + Database db = injectMockDatabase(); + + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeAndSave(player, world, true); + } + + verify(db).saveObject(any(InventoryStorage.class)); + verify(db, never()).saveObjectAsync(any(InventoryStorage.class)); + } + + /** + * The statistics branch returns early, so it needs its own check that a shutdown save is + * written synchronously. + */ + @Test + void testShutdownSaveIsSynchronousWithStatistics() throws Exception { + sets.setStatistics(true); + sets.setAdvancements(false); + Database db = injectMockDatabase(); + + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeAndSave(player, world, true); + } + + verify(db).saveObject(any(InventoryStorage.class)); + verify(db, never()).saveObjectAsync(any(InventoryStorage.class)); + } + + /** + * Normal (non-shutdown) saves must stay asynchronous so they do not block the main thread. + */ + @Test + void testNormalSaveStaysAsynchronous() throws Exception { + sets.setStatistics(false); + sets.setAdvancements(false); + Database db = injectMockDatabase(); + + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) { + s.storeAndSave(player, world, false); + } + + verify(db).saveObjectAsync(any(InventoryStorage.class)); + verify(db, never()).saveObject(any(InventoryStorage.class)); + } + } From f8d307c7e2ff199eb36189f01e031108174b5a72 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 1 Aug 2026 10:46:17 -0700 Subject: [PATCH 6/7] build: update JaCoCo to 0.8.15 for Java 25 support JaCoCo 0.8.12 cannot read class file major version 69, so instrumenting the classes Mockito generates at runtime failed with "Error while instrumenting ... Unsupported class file major version 69" whenever the build ran on a JDK 25 toolchain - which is what the CodeMC CI agent (OpenJDK_25) uses. 0.8.15 adds Java 25 class file support. Verified on JDK 25: mvn test now runs all 128 tests green with the agent attached, and jacoco:report analyzes the bundle and writes the report. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0183f7bfWoU9rmXxRCRwvrAs --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 59f6e5b..0204a31 100644 --- a/pom.xml +++ b/pom.xml @@ -323,7 +323,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.15 true From 9f6bbbf0dfae7807f5243e898224ca21e10e22e9 Mon Sep 17 00:00:00 2001 From: tastybento Date: Sat, 1 Aug 2026 10:55:57 -0700 Subject: [PATCH 7/7] config: add tradewinds_world to the default worlds list Pre-emptive, following the same audit as the other game mode worlds. As with those, an existing config.yml on disk is not rewritten, so this only affects fresh installs. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0183f7bfWoU9rmXxRCRwvrAs --- src/main/resources/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 04844d9..ec4a626 100755 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -13,6 +13,7 @@ worlds: - raft_world - brix_world - parkour_world +- tradewinds_world options: # # Per-world settings. Gamemode means Survivial, Creative, etc.