From 801f7c2b31b1459babad5c22f9df7945e14091c7 Mon Sep 17 00:00:00 2001 From: tastybento Date: Wed, 4 Nov 2026 20:41:39 -0800 Subject: [PATCH 1/3] Store the allow-mobs list in BentoBox, not in game mode configs Game modes only persist what their own Settings class declares, so a WorldSettings default method could never save admin edits and the panel appeared dead on every existing game mode release. Replace it with a BentoBox-stored record per game mode (CoreWorldSettings, table "WorldSettings"), loaded and saved through the normal database layer and seeded from GameModeAddon#getDefaultAllowedMobs() (IRON_GOLEM) on first use. No game mode changes are needed. Co-Authored-By: Claude Fable 5.1 --- .../bentobox/api/addons/GameModeAddon.java | 13 ++++ .../api/configuration/WorldSettings.java | 12 --- .../database/objects/CoreWorldSettings.java | 67 +++++++++++++++++ .../flags/clicklisteners/GeoMobLimitTab.java | 9 ++- .../bentobox/managers/IslandWorldManager.java | 73 ++++++++++++++++++- .../clicklisteners/GeoMobLimitTabTest.java | 6 +- .../managers/IslandWorldManagerTest.java | 62 ++++++++++++++++ 7 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 src/main/java/world/bentobox/bentobox/database/objects/CoreWorldSettings.java diff --git a/src/main/java/world/bentobox/bentobox/api/addons/GameModeAddon.java b/src/main/java/world/bentobox/bentobox/api/addons/GameModeAddon.java index 51f9ef93c..c807d14ab 100644 --- a/src/main/java/world/bentobox/bentobox/api/addons/GameModeAddon.java +++ b/src/main/java/world/bentobox/bentobox/api/addons/GameModeAddon.java @@ -1,5 +1,6 @@ package world.bentobox.bentobox.api.addons; +import java.util.List; import java.util.Optional; import org.bukkit.Location; @@ -73,6 +74,18 @@ public abstract class GameModeAddon extends Addon { */ public abstract WorldSettings getWorldSettings(); + /** + * Entity types that may always spawn in this game mode by default, regardless of the animal and + * monster spawning flags. Used to seed the BentoBox-stored world settings the first time this + * game mode is seen; after that the admin's edits in the settings panel apply. + * Override to change the seed, e.g. return an empty list for a game mode with no villages. + * @return list of {@link org.bukkit.entity.EntityType} names + * @since 3.23.6 + */ + public List getDefaultAllowedMobs() { + return List.of("IRON_GOLEM"); + } + /** * Checks if location is governed by this game mode * @param loc The location to check. diff --git a/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java b/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java index 84a6400fc..7bc423d86 100644 --- a/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java +++ b/src/main/java/world/bentobox/bentobox/api/configuration/WorldSettings.java @@ -427,18 +427,6 @@ default List getMobLimitSettings() { return new ArrayList<>(); } - /** - * Get list of entities that may always spawn in this game mode, regardless of the - * animal and monster spawning flags at world or island level. Entities listed in - * {@link #getMobLimitSettings()} are still blocked. Entries are {@link org.bukkit.entity.EntityType} - * names, e.g. {@code IRON_GOLEM}. - * @return list of entities that may always spawn - * @since 3.23.6 - */ - default List getMobAllowSettings() { - return new ArrayList<>(); - } - /** * @return reset limit for world */ diff --git a/src/main/java/world/bentobox/bentobox/database/objects/CoreWorldSettings.java b/src/main/java/world/bentobox/bentobox/database/objects/CoreWorldSettings.java new file mode 100644 index 000000000..6ec79afc9 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/database/objects/CoreWorldSettings.java @@ -0,0 +1,67 @@ +package world.bentobox.bentobox.database.objects; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gson.annotations.Expose; + +/** + * World-level settings that BentoBox stores itself, one record per game mode, keyed by the + * game mode addon's name. Unlike {@link world.bentobox.bentobox.api.configuration.WorldSettings}, + * which each game mode implements and saves in its own config.yml, these settings need no + * game mode support: BentoBox loads, seeds and saves them through its database. + *

+ * Read and edit them via {@link world.bentobox.bentobox.managers.IslandWorldManager}. + * @author tastybento + * @since 3.23.6 + */ +@Table(name = "WorldSettings") +public class CoreWorldSettings implements DataObject { + + @Expose + private String uniqueId = ""; // game mode addon name + + /** + * Entity types that may always spawn, regardless of the animal and monster spawning flags + */ + @Expose + private List allowedMobs = new ArrayList<>(); + + public CoreWorldSettings() { + // Required for deserialization + } + + /** + * @param gameModeName - name of the game mode addon these settings belong to + */ + public CoreWorldSettings(String gameModeName) { + this.uniqueId = gameModeName; + } + + @Override + public String getUniqueId() { + return uniqueId; + } + + @Override + public void setUniqueId(String uniqueId) { + this.uniqueId = uniqueId; + } + + /** + * @return mutable list of entity type names that may always spawn + */ + public List getAllowedMobs() { + if (allowedMobs == null) { + allowedMobs = new ArrayList<>(); + } + return allowedMobs; + } + + /** + * @param allowedMobs the entity type names that may always spawn + */ + public void setAllowedMobs(List allowedMobs) { + this.allowedMobs = allowedMobs; + } +} diff --git a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java index 33afeaba9..1de11d231 100644 --- a/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java +++ b/src/main/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTab.java @@ -99,9 +99,10 @@ public boolean onClick(Panel panel, User user, ClickType clickType, int slot) { // Mutate the game mode's live list - the IWM getters return an immutable // empty list when the world is not a game world (java:S6322) plugin.getIWM().getAddon(Util.getWorld(world)).ifPresent(gameModeAddon -> { + // The allow list is stored by BentoBox, not in the game mode's config List settings = switch (type) { case MOB_LIMIT -> gameModeAddon.getWorldSettings().getMobLimitSettings(); - case MOB_ALLOW -> gameModeAddon.getWorldSettings().getMobAllowSettings(); + case MOB_ALLOW -> plugin.getIWM().getMobAllowSettings(world); case GEO_LIMIT -> gameModeAddon.getWorldSettings().getGeoLimitSettings(); }; if (settings.contains(c.name())) { @@ -110,7 +111,11 @@ public boolean onClick(Panel panel, User user, ClickType clickType, int slot) { settings.add(c.name()); } // Save settings - gameModeAddon.saveWorldSettings(); + if (type == EntityLimitTabType.MOB_ALLOW) { + plugin.getIWM().saveCoreWorldSettings(world); + } else { + gameModeAddon.saveWorldSettings(); + } }); // Apply change to panel panel.getInventory().setItem(slot, getPanelItem(c, user).getItem()); diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java index 0cf3f127e..847cbae8c 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java @@ -1,6 +1,7 @@ package world.bentobox.bentobox.managers; import java.io.File; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -26,6 +27,8 @@ import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.hooks.Hook; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.CoreWorldSettings; import world.bentobox.bentobox.hooks.WorldManagementHook; import world.bentobox.bentobox.lists.Flags; @@ -42,6 +45,17 @@ public class IslandWorldManager { */ private final Map<@NonNull World, @NonNull GameModeAddon> gameModes; + /** + * World settings that BentoBox stores itself, cached per game mode name. + * @see #getCoreWorldSettings(World) + */ + private final Map coreWorldSettings = new HashMap<>(); + + /** + * Lazily created so that construction does not depend on the database being ready + */ + private Database coreWorldSettingsDb; + /** * Manages worlds registered with BentoBox */ @@ -859,14 +873,65 @@ public List getMobLimitSettings(@NonNull World world) { /** * Get the mob types that may always spawn in this world, regardless of the animal and - * monster spawning flags + * monster spawning flags. Stored by BentoBox, not by the game mode; seeded from + * {@link GameModeAddon#getDefaultAllowedMobs()}. Edit the returned list and then call + * {@link #saveCoreWorldSettings(World)}. * @param world - world - * @return list of always-allowed mobs + * @return mutable list of always-allowed mob type names, or an immutable empty list if not a game world * @since 3.23.6 */ public List getMobAllowSettings(@NonNull World world) { - return gameModes.containsKey(world) ? gameModes.get(world).getWorldSettings().getMobAllowSettings() - : Collections.emptyList(); + return getCoreWorldSettings(world).map(CoreWorldSettings::getAllowedMobs).orElse(Collections.emptyList()); + } + + /** + * Get the BentoBox-stored world settings for the game mode of this world, loading them from + * the database or seeding them from the game mode's defaults on first use. + * @param world - world + * @return the settings, or empty if the world is not a game world + * @since 3.23.6 + */ + public Optional getCoreWorldSettings(@NonNull World world) { + GameModeAddon gm = gameModes.get(world); + if (gm == null) { + return Optional.empty(); + } + String name = gm.getDescription().getName(); + return Optional.of(coreWorldSettings.computeIfAbsent(name, k -> loadOrSeed(gm, k))); + } + + private CoreWorldSettings loadOrSeed(GameModeAddon gm, String name) { + CoreWorldSettings s = getCoreWorldSettingsDb().loadObject(name); + if (s == null) { + s = new CoreWorldSettings(name); + s.setAllowedMobs(new ArrayList<>(gm.getDefaultAllowedMobs())); + getCoreWorldSettingsDb().saveObjectAsync(s); + } + return s; + } + + /** + * Save the BentoBox-stored world settings for the game mode of this world + * @param world - world + * @since 3.23.6 + */ + public void saveCoreWorldSettings(@NonNull World world) { + getCoreWorldSettings(world).ifPresent(s -> getCoreWorldSettingsDb().saveObjectAsync(s)); + } + + private Database getCoreWorldSettingsDb() { + if (coreWorldSettingsDb == null) { + coreWorldSettingsDb = new Database<>(plugin, CoreWorldSettings.class); + } + return coreWorldSettingsDb; + } + + /** + * Inject the database used for BentoBox-stored world settings. For tests. + * @param db - database + */ + void setCoreWorldSettingsDb(Database db) { + this.coreWorldSettingsDb = db; } /** diff --git a/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java b/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java index 201ac273d..d1ea2e6e7 100644 --- a/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java +++ b/src/test/java/world/bentobox/bentobox/listeners/flags/clicklisteners/GeoMobLimitTabTest.java @@ -6,6 +6,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -70,7 +71,6 @@ public void setUp() throws Exception { when(gma.getWorldSettings()).thenReturn(ws); when(ws.getMobLimitSettings()).thenReturn(list); when(ws.getGeoLimitSettings()).thenReturn(list); - when(ws.getMobAllowSettings()).thenReturn(list); // Panel when(panel.getInventory()).thenReturn(inv); // User @@ -186,7 +186,9 @@ void testOnClickMobAllow() { tab.onClick(panel, user, ClickType.LEFT, 10); assertEquals(4, list.size()); assertTrue(list.contains("ARMADILLO")); - verify(gma, times(2)).saveWorldSettings(); + // Stored by BentoBox, not the game mode + verify(iwm, times(2)).saveCoreWorldSettings(world); + verify(gma, never()).saveWorldSettings(); } /** diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java index 16d5503a5..115901165 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java @@ -3,8 +3,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.File; @@ -31,9 +37,12 @@ import org.mockito.Mockito; import world.bentobox.bentobox.CommonTestSetup; +import world.bentobox.bentobox.api.addons.AddonDescription; import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.flags.Flag; +import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.CoreWorldSettings; /** * @author tastybento @@ -673,6 +682,59 @@ void testGetGeoLimitSettings() { assertEquals(list, testIwm.getGeoLimitSettings(testWorld)); } + /** + * Test method for {@link world.bentobox.bentobox.managers.IslandWorldManager#getMobAllowSettings(org.bukkit.World)}. + */ + @Test + void testGetMobAllowSettingsSeeded() { + @SuppressWarnings("unchecked") + Database db = mock(Database.class); + testIwm.setCoreWorldSettingsDb(db); + AddonDescription desc = mock(AddonDescription.class); + when(desc.getName()).thenReturn("BSkyBlock"); + when(gm.getDescription()).thenReturn(desc); + when(gm.getDefaultAllowedMobs()).thenReturn(List.of("IRON_GOLEM")); + when(db.loadObject("BSkyBlock")).thenReturn(null); + + List list = testIwm.getMobAllowSettings(testWorld); + assertEquals(List.of("IRON_GOLEM"), list); + // Seeded record is saved + verify(db).saveObjectAsync(any(CoreWorldSettings.class)); + // Same list on every call: cached, loaded once + list.add("COW"); + assertSame(list, testIwm.getMobAllowSettings(testWorld)); + verify(db, times(1)).loadObject(anyString()); + + testIwm.saveCoreWorldSettings(testWorld); + verify(db, times(2)).saveObjectAsync(any(CoreWorldSettings.class)); + } + + @Test + void testGetMobAllowSettingsLoaded() { + @SuppressWarnings("unchecked") + Database db = mock(Database.class); + testIwm.setCoreWorldSettingsDb(db); + AddonDescription desc = mock(AddonDescription.class); + when(desc.getName()).thenReturn("BSkyBlock"); + when(gm.getDescription()).thenReturn(desc); + CoreWorldSettings stored = new CoreWorldSettings("BSkyBlock"); + stored.setAllowedMobs(new ArrayList<>(List.of("COW"))); + when(db.loadObject("BSkyBlock")).thenReturn(stored); + + assertSame(stored.getAllowedMobs(), testIwm.getMobAllowSettings(testWorld)); + assertEquals(List.of("COW"), testIwm.getMobAllowSettings(testWorld)); + verify(db, never()).saveObjectAsync(any()); + verify(gm, never()).getDefaultAllowedMobs(); + } + + @Test + void testGetMobAllowSettingsNotGameWorld() { + World other = mock(World.class); + assertTrue(testIwm.getMobAllowSettings(other).isEmpty()); + assertTrue(testIwm.getCoreWorldSettings(other).isEmpty()); + testIwm.saveCoreWorldSettings(other); // no-op + } + /** * Test method for {@link world.bentobox.bentobox.managers.IslandWorldManager#getResetLimit(org.bukkit.World)}. */ From 7f52c471dd8cd50f9b1e4e94712ea3853ed59945 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 5 Nov 2026 07:32:35 -0800 Subject: [PATCH 2/3] Check the record exists before loading core world settings The flat-file handlers log "Could not load file ... File not found" when loadObject is asked for a missing record, so the first-run seed path produced a spurious error. Ask objectExists first. Co-Authored-By: Claude Fable 5.1 --- .../bentobox/bentobox/managers/IslandWorldManager.java | 4 +++- .../bentobox/bentobox/managers/IslandWorldManagerTest.java | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java index 847cbae8c..206d57d72 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java @@ -901,7 +901,9 @@ public Optional getCoreWorldSettings(@NonNull World world) { } private CoreWorldSettings loadOrSeed(GameModeAddon gm, String name) { - CoreWorldSettings s = getCoreWorldSettingsDb().loadObject(name); + // Check first: loading a missing record logs an error in the flat-file handlers + CoreWorldSettings s = getCoreWorldSettingsDb().objectExists(name) ? getCoreWorldSettingsDb().loadObject(name) + : null; if (s == null) { s = new CoreWorldSettings(name); s.setAllowedMobs(new ArrayList<>(gm.getDefaultAllowedMobs())); diff --git a/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java b/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java index 115901165..449029b9d 100644 --- a/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java +++ b/src/test/java/world/bentobox/bentobox/managers/IslandWorldManagerTest.java @@ -694,16 +694,18 @@ void testGetMobAllowSettingsSeeded() { when(desc.getName()).thenReturn("BSkyBlock"); when(gm.getDescription()).thenReturn(desc); when(gm.getDefaultAllowedMobs()).thenReturn(List.of("IRON_GOLEM")); - when(db.loadObject("BSkyBlock")).thenReturn(null); + when(db.objectExists("BSkyBlock")).thenReturn(false); List list = testIwm.getMobAllowSettings(testWorld); assertEquals(List.of("IRON_GOLEM"), list); + // A missing record is never loaded, which would log an error + verify(db, never()).loadObject(anyString()); // Seeded record is saved verify(db).saveObjectAsync(any(CoreWorldSettings.class)); // Same list on every call: cached, loaded once list.add("COW"); assertSame(list, testIwm.getMobAllowSettings(testWorld)); - verify(db, times(1)).loadObject(anyString()); + verify(db, times(1)).objectExists(anyString()); testIwm.saveCoreWorldSettings(testWorld); verify(db, times(2)).saveObjectAsync(any(CoreWorldSettings.class)); @@ -719,6 +721,7 @@ void testGetMobAllowSettingsLoaded() { when(gm.getDescription()).thenReturn(desc); CoreWorldSettings stored = new CoreWorldSettings("BSkyBlock"); stored.setAllowedMobs(new ArrayList<>(List.of("COW"))); + when(db.objectExists("BSkyBlock")).thenReturn(true); when(db.loadObject("BSkyBlock")).thenReturn(stored); assertSame(stored.getAllowedMobs(), testIwm.getMobAllowSettings(testWorld)); From 887fd1f78652106e2b0b925ffb23264f1f5195b5 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 5 Nov 2026 07:50:33 -0800 Subject: [PATCH 3/3] Address Sonar S2583: guard with containsKey, not a null check on a @NonNull-valued map Co-Authored-By: Claude Fable 5.1 --- .../world/bentobox/bentobox/managers/IslandWorldManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java index 206d57d72..079a6e1f4 100644 --- a/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java +++ b/src/main/java/world/bentobox/bentobox/managers/IslandWorldManager.java @@ -892,10 +892,10 @@ public List getMobAllowSettings(@NonNull World world) { * @since 3.23.6 */ public Optional getCoreWorldSettings(@NonNull World world) { - GameModeAddon gm = gameModes.get(world); - if (gm == null) { + if (!gameModes.containsKey(world)) { return Optional.empty(); } + GameModeAddon gm = gameModes.get(world); String name = gm.getDescription().getName(); return Optional.of(coreWorldSettings.computeIfAbsent(name, k -> loadOrSeed(gm, k))); }