Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package world.bentobox.bentobox.api.addons;

import java.util.List;
import java.util.Optional;

import org.bukkit.Location;
Expand Down Expand Up @@ -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<String> getDefaultAllowedMobs() {
return List.of("IRON_GOLEM");
}

/**
* Checks if location is governed by this game mode
* @param loc The location to check.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,18 +427,6 @@ default List<String> 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<String> getMobAllowSettings() {
return new ArrayList<>();
}

/**
* @return reset limit for world
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<String> 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<String> getAllowedMobs() {
if (allowedMobs == null) {
allowedMobs = new ArrayList<>();
}
return allowedMobs;
}

/**
* @param allowedMobs the entity type names that may always spawn
*/
public void setAllowedMobs(List<String> allowedMobs) {
this.allowedMobs = allowedMobs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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())) {
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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<String, CoreWorldSettings> coreWorldSettings = new HashMap<>();

/**
* Lazily created so that construction does not depend on the database being ready
*/
private Database<CoreWorldSettings> coreWorldSettingsDb;

/**
* Manages worlds registered with BentoBox
*/
Expand Down Expand Up @@ -859,14 +873,67 @@ public List<String> 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<String> 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<CoreWorldSettings> getCoreWorldSettings(@NonNull World world) {
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)));
}

private CoreWorldSettings loadOrSeed(GameModeAddon gm, String 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()));
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<CoreWorldSettings> 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<CoreWorldSettings> db) {
this.coreWorldSettingsDb = db;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -673,6 +682,62 @@ 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<CoreWorldSettings> 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.objectExists("BSkyBlock")).thenReturn(false);

List<String> 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)).objectExists(anyString());

testIwm.saveCoreWorldSettings(testWorld);
verify(db, times(2)).saveObjectAsync(any(CoreWorldSettings.class));
}

@Test
void testGetMobAllowSettingsLoaded() {
@SuppressWarnings("unchecked")
Database<CoreWorldSettings> 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.objectExists("BSkyBlock")).thenReturn(true);
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)}.
*/
Expand Down
Loading