diff --git a/src/main/java/com/wasteofplastic/invswitcher/Settings.java b/src/main/java/com/wasteofplastic/invswitcher/Settings.java index 247e802..6d57227 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/Settings.java +++ b/src/main/java/com/wasteofplastic/invswitcher/Settings.java @@ -35,6 +35,28 @@ public class Settings implements ConfigObject { @ConfigEntry(path = "options.statistics") private boolean statistics = true; + @ConfigComment("Switch inventories based on island. Only applies if players own more than one island.") + @ConfigComment("Each sub-option controls whether that aspect is switched per-island.") + @ConfigComment("The world-level option must also be true for the island option to have any effect.") + @ConfigEntry(path = "options.islands.active") + private boolean islandsActive = true; + @ConfigEntry(path = "options.islands.inventory") + private boolean islandsInventory = true; + @ConfigEntry(path = "options.islands.health") + private boolean islandsHealth = false; + @ConfigEntry(path = "options.islands.food") + private boolean islandsFood = false; + @ConfigEntry(path = "options.islands.advancements") + private boolean islandsAdvancements = false; + @ConfigEntry(path = "options.islands.gamemode") + private boolean islandsGamemode = false; + @ConfigEntry(path = "options.islands.experience") + private boolean islandsExperience = false; + @ConfigEntry(path = "options.islands.ender-chest") + private boolean islandsEnderChest = true; + @ConfigEntry(path = "options.islands.statistics") + private boolean islandsStatistics = false; + /** * @return the worlds */ @@ -143,6 +165,65 @@ public boolean isStatistics() { public void setStatistics(boolean statistics) { this.statistics = statistics; } - + /** + * @return whether per-island switching is active + */ + public boolean isIslandsActive() { + return islandsActive; + } + /** + * @param islandsActive whether to enable per-island switching + */ + public void setIslandsActive(boolean islandsActive) { + this.islandsActive = islandsActive; + } + public boolean isIslandsInventory() { + return islandsInventory; + } + public void setIslandsInventory(boolean islandsInventory) { + this.islandsInventory = islandsInventory; + } + public boolean isIslandsHealth() { + return islandsHealth; + } + public void setIslandsHealth(boolean islandsHealth) { + this.islandsHealth = islandsHealth; + } + public boolean isIslandsFood() { + return islandsFood; + } + public void setIslandsFood(boolean islandsFood) { + this.islandsFood = islandsFood; + } + public boolean isIslandsAdvancements() { + return islandsAdvancements; + } + public void setIslandsAdvancements(boolean islandsAdvancements) { + this.islandsAdvancements = islandsAdvancements; + } + public boolean isIslandsGamemode() { + return islandsGamemode; + } + public void setIslandsGamemode(boolean islandsGamemode) { + this.islandsGamemode = islandsGamemode; + } + public boolean isIslandsExperience() { + return islandsExperience; + } + public void setIslandsExperience(boolean islandsExperience) { + this.islandsExperience = islandsExperience; + } + public boolean isIslandsEnderChest() { + return islandsEnderChest; + } + public void setIslandsEnderChest(boolean islandsEnderChest) { + this.islandsEnderChest = islandsEnderChest; + } + public boolean isIslandsStatistics() { + return islandsStatistics; + } + public void setIslandsStatistics(boolean islandsStatistics) { + this.islandsStatistics = islandsStatistics; + } } diff --git a/src/main/java/com/wasteofplastic/invswitcher/Store.java b/src/main/java/com/wasteofplastic/invswitcher/Store.java index cb392db..41e1362 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/Store.java +++ b/src/main/java/com/wasteofplastic/invswitcher/Store.java @@ -30,11 +30,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Registry; import org.bukkit.Statistic; @@ -49,7 +51,9 @@ import com.wasteofplastic.invswitcher.dataobjects.InventoryStorage; +import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.database.Database; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; /** @@ -62,12 +66,109 @@ public class Store { private static final CharSequence NETHER = "_nether"; private final Database database; private final Map cache; + private final Map currentKey; private final InvSwitcher addon; public Store(InvSwitcher addon) { this.addon = addon; database = new Database<>(addon, InventoryStorage.class); cache = new HashMap<>(); + currentKey = new HashMap<>(); + } + + /** + * Compute the storage key for a player based on their current location. + * Returns "worldName/islandId" if per-island mode is active and the player + * owns multiple concurrent islands, otherwise returns just "worldName". + * @param player - player + * @param world - world + * @return storage key + */ + public String getStorageKey(Player player, World world) { + return getStorageKey(player, world, player.getLocation(), null); + } + + /** + * Compute the storage key for a player targeting a specific island. + * @param player - player + * @param world - world + * @param island - target island (may be null) + * @return storage key + */ + public String getStorageKey(Player player, World world, Island island) { + return getStorageKey(player, world, player.getLocation(), island); + } + + /** + * Compute the storage key for a player at a specific location, optionally targeting a known island. + * @param player - player + * @param world - world + * @param location - location to check + * @param island - target island, or null to detect from location + * @return storage key + */ + String getStorageKey(Player player, World world, Location location, Island island) { + String overworldName = getOverworldName(world); + + if (!addon.getSettings().isIslandsActive()) { + return overworldName; + } + + // Check if player owns multiple concurrent islands in this world + World overworld = Util.getWorld(world); + int count = addon.getIslands().getNumberOfConcurrentIslands(player.getUniqueId(), + Objects.requireNonNull(overworld)); + if (count <= 1) { + return overworldName; + } + + // If a specific island was provided, use it + if (island != null && island.getOwner() != null + && island.getOwner().equals(player.getUniqueId())) { + return overworldName + "/" + island.getUniqueId(); + } + + // If in generic nether/end (not island nether/end), preserve current key + if (world.getEnvironment() != World.Environment.NORMAL) { + boolean isIslandDimension = (world.getEnvironment() == World.Environment.NETHER) + ? BentoBox.getInstance().getIWM().isIslandNether(world) + : BentoBox.getInstance().getIWM().isIslandEnd(world); + if (!isIslandDimension) { + String current = currentKey.get(player.getUniqueId()); + return (current != null) ? current : overworldName; + } + } + + // Detect island from location + Optional islandOpt = addon.getIslands().getIslandAt(location); + if (islandOpt.isPresent()) { + Island loc = islandOpt.get(); + if (loc.getOwner() != null && loc.getOwner().equals(player.getUniqueId())) { + return overworldName + "/" + loc.getUniqueId(); + } + } + + // Fallback: use current key if available, else world name + String current = currentKey.get(player.getUniqueId()); + return (current != null) ? current : overworldName; + } + + /** + * Get the overworld name from any world by stripping nether/end suffixes. + * @param world - world + * @return overworld name + */ + private String getOverworldName(World world) { + return (world.getName().replace(THE_END, "")).replace(NETHER, ""); + } + + /** + * Get the current storage key for a player. + * @param player - player + * @return the current storage key, or null if not set + */ + public String getCurrentKey(Player player) { + return currentKey.get(player.getUniqueId()); } /** @@ -79,8 +180,8 @@ public Store(InvSwitcher addon) { public boolean isWorldStored(Player player, World world) { // Get the store InventoryStorage store = getInv(player); - String overworldName = (world.getName().replace(THE_END, "")).replace(NETHER, ""); - return store.isInventory(overworldName); + String key = getStorageKey(player, world); + return store.isInventory(key); } /** @@ -89,38 +190,70 @@ public boolean isWorldStored(Player player, World world) { * @param world - world */ public void getInventory(Player player, World world) { + getInventory(player, world, null); + } + + /** + * Gets items for world and island. Changes the inventory of player immediately. + * @param player - player + * @param world - world + * @param island - target island, or null to detect from location + */ + public void getInventory(Player player, World world, Island island) { // Get the store InventoryStorage store = getInv(player); - // Do not differentiate between world environments. - String overworldName = Objects.requireNonNull(Util.getWorld(world)).getName(); + String islandKey = (island != null) ? getStorageKey(player, world, island) : getStorageKey(player, world); + String worldKey = getOverworldName(world); + + // Always track the island-level key so future saves and island detection work correctly + currentKey.put(player.getUniqueId(), islandKey); + + // Backward compat: if island-specific key has no data, migrate from world-only key. + // This only happens once — the world-only data is cleared after migration so that + // other islands don't also inherit a duplicate copy. + String islandLoadKey = islandKey; + if (islandKey.contains("/") && !store.isInventory(islandKey)) { + if (store.isInventory(worldKey)) { + islandLoadKey = worldKey; + // Clear the world-only data so it can't be claimed by another island + store.clearWorldData(worldKey); + } + } - // Inventory - if (addon.getSettings().isInventory()) { - player.getInventory().setContents(store.getInventory(overworldName).toArray(new ItemStack[0])); + // Each option uses the island key or the world key based on its island sub-setting + Settings settings = addon.getSettings(); + if (settings.isInventory()) { + String k = settings.isIslandsInventory() ? islandLoadKey : worldKey; + player.getInventory().setContents(store.getInventory(k).toArray(new ItemStack[0])); } - if (addon.getSettings().isHealth()) { - setHeath(store, player, overworldName); + if (settings.isHealth()) { + String k = settings.isIslandsHealth() ? islandLoadKey : worldKey; + setHeath(store, player, k); } - if (addon.getSettings().isFood()) { - setFood(store, player, overworldName); + if (settings.isFood()) { + String k = settings.isIslandsFood() ? islandLoadKey : worldKey; + setFood(store, player, k); } - if (addon.getSettings().isExperience()) { - // Experience - setTotalExperience(player, store.getExp().getOrDefault(overworldName, 0)); + if (settings.isExperience()) { + String k = settings.isIslandsExperience() ? islandLoadKey : worldKey; + setTotalExperience(player, store.getExp().getOrDefault(k, 0)); } - if (addon.getSettings().isGamemode()) { - // Game modes - player.setGameMode(store.getGameMode(overworldName)); + if (settings.isGamemode()) { + String k = settings.isIslandsGamemode() ? islandLoadKey : worldKey; + player.setGameMode(store.getGameMode(k)); } - if (addon.getSettings().isAdvancements()) { - setAdvancements(store, player, overworldName); + if (settings.isAdvancements()) { + String k = settings.isIslandsAdvancements() ? islandLoadKey : worldKey; + setAdvancements(store, player, k); } - if (addon.getSettings().isEnderChest()) { - player.getEnderChest().setContents(store.getEnderChest(overworldName).toArray(new ItemStack[0])); + if (settings.isEnderChest()) { + String k = settings.isIslandsEnderChest() ? islandLoadKey : worldKey; + player.getEnderChest().setContents(store.getEnderChest(k).toArray(new ItemStack[0])); } - if (addon.getSettings().isStatistics()) { - getStats(store, player, overworldName); + if (settings.isStatistics()) { + String k = settings.isIslandsStatistics() ? islandLoadKey : worldKey; + getStats(store, player, k); } } @@ -169,6 +302,7 @@ private void setAdvancements(InventoryStorage store, Player player, String overw public void removeFromCache(Player player) { cache.remove(player.getUniqueId()); + currentKey.remove(player.getUniqueId()); } /** @@ -213,45 +347,53 @@ public void storeInventory(Player player, World world) { public void storeAndSave(Player player, World world, boolean shutdown) { // Get the player's store InventoryStorage store = getInv(player); - // Do not differentiate between world environments - String worldName = world.getName(); - String overworldName = (world.getName().replace(THE_END, "")).replace(NETHER, ""); - if (addon.getSettings().isInventory()) { - // Copy the player's items to the store + // Use the current tracked key if available (ensures we save to the correct island slot), + // otherwise compute from location + String islandKey = currentKey.getOrDefault(player.getUniqueId(), getStorageKey(player, world)); + String worldKey = getOverworldName(world); + // Each option saves to the island key or the world key based on its island sub-setting + Settings settings = addon.getSettings(); + if (settings.isInventory()) { + String k = settings.isIslandsInventory() ? islandKey : worldKey; List contents = Arrays.asList(player.getInventory().getContents()); - store.setInventory(overworldName, contents); + store.setInventory(k, contents); } - if (addon.getSettings().isHealth()) { - store.setHealth(overworldName, player.getHealth()); + if (settings.isHealth()) { + String k = settings.isIslandsHealth() ? islandKey : worldKey; + store.setHealth(k, player.getHealth()); } - if (addon.getSettings().isFood()) { - store.setFood(overworldName, player.getFoodLevel()); + if (settings.isFood()) { + String k = settings.isIslandsFood() ? islandKey : worldKey; + store.setFood(k, player.getFoodLevel()); } - if (addon.getSettings().isExperience()) { - store.setExp(overworldName, getTotalExperience(player)); + if (settings.isExperience()) { + String k = settings.isIslandsExperience() ? islandKey : worldKey; + store.setExp(k, getTotalExperience(player)); } - if (addon.getSettings().isGamemode()) { - store.setGameMode(overworldName, player.getGameMode()); + if (settings.isGamemode()) { + String k = settings.isIslandsGamemode() ? islandKey : worldKey; + store.setGameMode(k, player.getGameMode()); } - if (addon.getSettings().isAdvancements()) { - // Advancements - store.clearAdvancement(worldName); + if (settings.isAdvancements()) { + String k = settings.isIslandsAdvancements() ? islandKey : worldKey; + store.clearAdvancement(k); Iterator it = Bukkit.advancementIterator(); while (it.hasNext()) { Advancement a = it.next(); AdvancementProgress p = player.getAdvancementProgress(a); if (!p.getAwardedCriteria().isEmpty()) { - store.setAdvancement(worldName, a.getKey().toString(), new ArrayList<>(p.getAwardedCriteria())); + store.setAdvancement(k, a.getKey().toString(), new ArrayList<>(p.getAwardedCriteria())); } } } - if (addon.getSettings().isEnderChest()) { - // Copy the player's ender chest items to the store + if (settings.isEnderChest()) { + String k = settings.isIslandsEnderChest() ? islandKey : worldKey; List contents = Arrays.asList(player.getEnderChest().getContents()); - store.setEnderChest(overworldName, contents); + store.setEnderChest(k, contents); } - if (addon.getSettings().isStatistics()) { - saveStats(store, player, overworldName, shutdown).thenAccept(database::saveObjectAsync); + if (settings.isStatistics()) { + String k = settings.isIslandsStatistics() ? islandKey : worldKey; + saveStats(store, player, k, shutdown).thenAccept(database::saveObjectAsync); return; } database.saveObjectAsync(store); diff --git a/src/main/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorage.java b/src/main/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorage.java index 48c2b8e..dcf4cbf 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorage.java +++ b/src/main/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorage.java @@ -22,45 +22,104 @@ @Table(name = "InventoryStorage") public class InventoryStorage implements DataObject { + /** + * The unique identifier for this inventory storage. + */ @Expose private String uniqueId; + + /** + * Map of world name to inventory contents. + */ @Expose private Map> inventory = new HashMap<>(); + + /** + * Map of world name to player health. + */ @Expose private Map health = new HashMap<>(); + + /** + * Map of world name to player food level. + */ @Expose private Map food = new HashMap<>(); + + /** + * Map of world name to player experience. + */ @Expose private Map exp = new HashMap<>(); + + /** + * Map of world name to player location. + */ @Expose private Map location = new HashMap<>(); + + /** + * Map of world name to player game mode. + */ @Expose private Map gameMode = new HashMap<>(); + + /** + * Map of world name to advancements (keyed by advancement key and criteria). + */ @Expose private Map>> advancements = new HashMap<>(); + + /** + * Map of world name to Ender Chest inventory contents. + */ @Expose private Map> enderChest = new HashMap<>(); + + /** + * Map of world name to untyped statistics. + */ @Expose private Map> untypedStats = new HashMap<>(); + + /** + * Map of world name to block statistics. + */ @Expose private Map>> blockStats = new HashMap<>(); + + /** + * Map of world name to item statistics. + */ @Expose private Map>> itemStats = new HashMap<>(); + + /** + * Map of world name to entity statistics. + */ @Expose private Map>> entityStats = new HashMap<>(); + /** + * Gets the unique identifier for this inventory storage. + * @return the uniqueId + */ @Override public String getUniqueId() { return uniqueId; } + /** + * Sets the unique identifier for this inventory storage. + * @param uniqueId the uniqueId to set + */ @Override public void setUniqueId(String uniqueId) { this.uniqueId = uniqueId; - } /** + * Gets the inventory map. * @return the inventory */ public Map> getInventory() { @@ -68,6 +127,7 @@ public Map> getInventory() { } /** + * Gets the health map. * @return the health */ public Map getHealth() { @@ -75,6 +135,7 @@ public Map getHealth() { } /** + * Gets the food map. * @return the food */ public Map getFood() { @@ -82,6 +143,7 @@ public Map getFood() { } /** + * Gets the experience map. * @return the exp */ public Map getExp() { @@ -89,14 +151,15 @@ public Map getExp() { } /** + * Gets the location map. * @return the location */ public Map getLocation() { return location; } - /** + * Sets the inventory map. * @param inventory the inventory to set */ public void setInventory(Map> inventory) { @@ -104,16 +167,16 @@ public void setInventory(Map> inventory) { } /** - * + * Sets the inventory for a specific world. * @param worldname the world name * @param inventory the inventory to set */ public void setInventory(String worldname, List inventory) { this.inventory.put(worldname, inventory); - } /** + * Sets the health map. * @param health the health to set */ public void setHealth(Map health) { @@ -121,6 +184,7 @@ public void setHealth(Map health) { } /** + * Sets the food map. * @param food the food to set */ public void setFood(Map food) { @@ -128,6 +192,7 @@ public void setFood(Map food) { } /** + * Sets the experience map. * @param exp the exp to set */ public void setExp(Map exp) { @@ -135,108 +200,149 @@ public void setExp(Map exp) { } /** + * Sets the location map. * @param location the location to set */ public void setLocation(Map location) { this.location = location; } + /** + * Sets the health for a specific world. + * @param overworldName the world name + * @param health2 the health value to set + */ public void setHealth(String overworldName, double health2) { this.health.put(overworldName, health2); - } + /** + * Sets the food level for a specific world. + * @param overworldName the world name + * @param foodLevel the food level to set + */ public void setFood(String overworldName, int foodLevel) { this.food.put(overworldName, foodLevel); - } + /** + * Sets the experience for a specific world. + * @param overworldName the world name + * @param totalExperience the experience value to set + */ public void setExp(String overworldName, int totalExperience) { this.exp.put(overworldName, totalExperience); - } + /** + * Sets the location for a specific world. + * @param worldName the world name + * @param location2 the location to set + */ public void setLocation(String worldName, Location location2) { this.location.put(worldName, location2); - } + /** + * Gets the inventory for a specific world. + * @param overworldName the world name + * @return the inventory list + */ public List getInventory(String overworldName) { return inventory == null ? new ArrayList<>() : inventory.getOrDefault(overworldName, new ArrayList<>()); } /** - * Check if an inventory for this world exists or not - * @param overworldName - over world name - * @return true if there is an inventory for this world, false if not. + * Checks if an inventory exists for a specific world. + * @param overworldName the world name + * @return true if inventory exists, false otherwise */ public boolean isInventory(String overworldName) { return inventory != null && inventory.containsKey(overworldName); } + /** + * Sets the game mode for a specific world. + * @param worldName the world name + * @param gameMode the game mode to set + */ public void setGameMode(String worldName, GameMode gameMode) { this.gameMode.put(worldName, gameMode); } + /** + * Gets the game mode for a specific world. + * @param worldName the world name + * @return the game mode, or SURVIVAL if not set + */ public GameMode getGameMode(String worldName) { return this.gameMode.getOrDefault(worldName, GameMode.SURVIVAL); } + /** + * Sets an advancement for a specific world. + * @param worldName the world name + * @param key the advancement key + * @param criteria the advancement criteria + */ public void setAdvancement(String worldName, String key, List criteria) { this.advancements.computeIfAbsent(worldName, k -> new HashMap<>()).put(key, criteria); } /** - * Clears advancements for world - * @param worldName - world name + * Clears advancements for a specific world. + * @param worldName the world name */ public void clearAdvancement(String worldName) { this.advancements.remove(worldName); } /** - * @return the advancements + * Gets the advancements for a specific world. + * @param worldName the world name + * @return the advancements map */ public Map> getAdvancements(String worldName) { return advancements.getOrDefault(worldName, Collections.emptyMap()); } /** - * Get the EnderChest inventory - * @param overworldName - world name - * @return inventory + * Gets the Ender Chest inventory for a specific world. + * @param overworldName the world name + * @return the Ender Chest inventory list */ public List getEnderChest(String overworldName) { return enderChest == null ? new ArrayList<>() : enderChest.getOrDefault(overworldName, new ArrayList<>()); } /** - * + * Sets the Ender Chest inventory for a specific world. * @param worldname the world name * @param inventory the inventory to set */ public void setEnderChest(String worldname, List inventory) { this.enderChest.put(worldname, inventory); - } /** - * @return the enderChest + * Gets the Ender Chest inventory map. + * @return the enderChest map */ public Map> getEnderChest() { return enderChest; } /** - * @param enderChest the enderChest to set + * Sets the Ender Chest inventory map. + * @param enderChest the enderChest map to set */ public void setEnderChest(Map> enderChest) { this.enderChest = enderChest; } /** - * Clear the stats for player for world name - * @param worldName World name + * Clears all statistics for a player for a specific world. + * @param worldName the world name */ public void clearStats(String worldName) { this.blockStats.remove(worldName); @@ -246,69 +352,92 @@ public void clearStats(String worldName) { } /** - * Get Untyped stats - * @param worldName World name - * @return the untypedStats + * Gets the untyped statistics for a specific world. + * @param worldName the world name + * @return the untypedStats map */ public Map getUntypedStats(String worldName) { return untypedStats.computeIfAbsent(worldName, k -> new EnumMap<>(Statistic.class)); } /** - * @param worldName World name - * @param untypedStats the untypedStats to set + * Sets the untyped statistics for a specific world. + * @param worldName the world name + * @param untypedStats the untypedStats map to set */ public void setUntypedStats(String worldName, Map untypedStats) { this.untypedStats.put(worldName, untypedStats); } /** - * @param worldName World name - * @return the blockStats + * Gets the block statistics for a specific world. + * @param worldName the world name + * @return the blockStats map */ public Map> getBlockStats(String worldName) { return blockStats.computeIfAbsent(worldName, k -> new EnumMap<>(Statistic.class)); } /** - * @param worldName World name - * @param blockStats the blockStats to set + * Sets the block statistics for a specific world. + * @param worldName the world name + * @param blockStats the blockStats map to set */ public void setBlockStats(String worldName, Map> blockStats) { this.blockStats.put(worldName, blockStats); } /** - * @param worldName World name - * @return the itemStats + * Gets the item statistics for a specific world. + * @param worldName the world name + * @return the itemStats map */ public Map> getItemStats(String worldName) { return itemStats.computeIfAbsent(worldName, k -> new EnumMap<>(Statistic.class)); } /** - * @param worldName World name - * @param itemStats the itemStats to set + * Sets the item statistics for a specific world. + * @param worldName the world name + * @param itemStats the itemStats map to set */ public void setItemStats(String worldName, Map> itemStats) { this.itemStats.put(worldName, itemStats); } /** - * @param worldName World name - * @return the entityStats + * Gets the entity statistics for a specific world. + * @param worldName the world name + * @return the entityStats map */ public Map> getEntityStats(String worldName) { return entityStats.computeIfAbsent(worldName, k -> new EnumMap<>(Statistic.class)); } /** - * @param worldName World name - * @param entityStats the entityStats to set + * Sets the entity statistics for a specific world. + * @param worldName the world name + * @param entityStats the entityStats map to set */ public void setEntityStats(String worldName, Map> entityStats) { this.entityStats.put(worldName, entityStats); } + /** + * Clears all data for a specific world key. Used during migration from world-only + * keys to island-specific keys to prevent data duplication. + * @param worldName the world name key to clear + */ + public void clearWorldData(String worldName) { + this.inventory.remove(worldName); + this.health.remove(worldName); + this.food.remove(worldName); + this.exp.remove(worldName); + this.location.remove(worldName); + this.gameMode.remove(worldName); + this.advancements.remove(worldName); + this.enderChest.remove(worldName); + clearStats(worldName); + } } \ No newline at end of file diff --git a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java index 2574209..d23ee7c 100644 --- a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java +++ b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java @@ -1,5 +1,9 @@ package com.wasteofplastic.invswitcher.listeners; +import java.util.Objects; +import java.util.Optional; + +import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -7,9 +11,14 @@ import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; +import org.bukkit.entity.Player; import com.wasteofplastic.invswitcher.InvSwitcher; +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.events.island.IslandEnterEvent; +import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; /** @@ -52,6 +61,60 @@ public void onWorldEnter(final PlayerChangedWorldEvent event) { addon.getStore().getInventory(event.getPlayer(), to); } + /** + * Handles inventory switching when a player enters an island they own. + * Only triggers when per-island switching is enabled and the player owns multiple islands. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onIslandEnter(IslandEnterEvent event) { + BentoBox.getInstance().logDebug("IslandEnterEvent triggered for player " + event.getPlayerUUID() + " on island " + event.getIsland().getUniqueId()); + if (!addon.getSettings().isIslandsActive()) { + return; + } + + Player player = Bukkit.getPlayer(event.getPlayerUUID()); + if (player == null) { + return; + } + + World world = player.getWorld(); + if (!addon.getWorlds().contains(world)) { + BentoBox.getInstance().logDebug("World " + world.getName() + " is not in the list of worlds to manage. Ignoring island enter event."); + return; + } + + Island island = event.getIsland(); + + // Only switch if the player owns the island they're entering + if (island.getOwner() == null || !island.getOwner().equals(player.getUniqueId())) { + BentoBox.getInstance().logDebug("Player " + player.getName() + " does not own island " + island.getUniqueId() + ". No inventory switch."); + return; + } + + // Only switch if player owns multiple islands (otherwise key is the same) + World overworld = Util.getWorld(world); + int count = addon.getIslands().getNumberOfConcurrentIslands( + player.getUniqueId(), Objects.requireNonNull(overworld)); + if (count <= 1) { + BentoBox.getInstance().logDebug("Player " + player.getName() + " owns only one island in world " + overworld.getName() + ". No inventory switch."); + return; + } + + // Compute new key and compare to current key + String newKey = addon.getStore().getStorageKey(player, world, island); + String currentKeyValue = addon.getStore().getCurrentKey(player); + if (newKey.equals(currentKeyValue)) { + BentoBox.getInstance().logDebug("Player " + player.getName() + " is entering island " + island.getUniqueId() + " which has the same inventory key as their current location. No inventory switch."); + return; // same island, no switch + } + + // Switch: store old, load new + BentoBox.getInstance().logDebug("Switching inventory for player " + player.getName() + " from key " + currentKeyValue + " to new key " + newKey); + addon.getStore().storeInventory(player, world); + addon.getStore().getInventory(player, world, island); + } + /** * Loads inventory @@ -64,6 +127,41 @@ public void onPlayerJoin(final PlayerJoinEvent event) { } } + /** + * Handles inventory switching when a player respawns on a different island they own. + * @param event - event + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlayerRespawn(PlayerRespawnEvent event) { + if (!addon.getSettings().isIslandsActive()) { + return; + } + + Player player = event.getPlayer(); + World world = event.getRespawnLocation().getWorld(); + if (world == null || !addon.getWorlds().contains(world)) { + return; + } + + // Determine which island the respawn location is on + Optional islandOpt = addon.getIslands().getIslandAt(event.getRespawnLocation()); + if (islandOpt.isEmpty()) { + return; + } + + Island island = islandOpt.get(); + String newKey = addon.getStore().getStorageKey(player, world, island); + String currentKeyValue = addon.getStore().getCurrentKey(player); + + if (currentKeyValue != null && !newKey.equals(currentKeyValue)) { + // Player died on one island, respawning on another they own. + // Save the post-death state (empty inventory, etc.) to the old island key. + addon.getStore().storeAndSave(player, world, false); + // Load the respawn island's inventory + addon.getStore().getInventory(player, world, island); + } + } + /** * Saves inventory * @param event - event diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 31e0b37..34a19b8 100755 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -20,3 +20,16 @@ options: experience: true ender-chest: true statistics: true + # Switch inventories based on island. Only applies if players own more than one island. + # Each sub-option controls whether that aspect is switched per-island. + # The world-level option must also be true for the island option to have any effect. + islands: + active: true + inventory: true + health: false + food: false + advancements: false + gamemode: false + experience: false + ender-chest: true + statistics: false diff --git a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java index b5bc708..97f2e69 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java @@ -1,7 +1,9 @@ package com.wasteofplastic.invswitcher; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; @@ -20,10 +22,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Comparator; +import java.util.HashSet; +import java.util.Optional; import java.util.UUID; import java.util.logging.Logger; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; import org.bukkit.World.Environment; @@ -46,6 +51,9 @@ import world.bentobox.bentobox.BentoBox; import world.bentobox.bentobox.Settings; import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandWorldManager; +import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.util.Util; /** @@ -63,11 +71,15 @@ public class StoreTest { private World world; @Mock private Settings settings; + @Mock + private IslandsManager islandsManager; private Store s; private com.wasteofplastic.invswitcher.Settings sets; + private UUID playerUUID; + @Mock private Logger logger; @@ -88,8 +100,8 @@ public void setUp() when(plugin.getSettings()).thenReturn(settings); // Player mock - UUID uuid = UUID.randomUUID(); - when(player.getUniqueId()).thenReturn(uuid); + playerUUID = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(playerUUID); AttributeInstance attribute = mock(AttributeInstance.class); // Health when(attribute.getValue()).thenReturn(18D); @@ -114,6 +126,7 @@ public void setUp() // Addon when(addon.getLogger()).thenReturn(logger); + when(addon.getIslands()).thenReturn(islandsManager); //PowerMockito.mockStatic(Util.class); try (MockedStatic utilities = Mockito.mockStatic(Util.class)) { @@ -123,6 +136,9 @@ public void setUp() DatabaseType mockDbt = mock(DatabaseType.class); when(settings.getDatabaseType()).thenReturn(mockDbt); + // Disable island switching by default for existing tests + sets.setIslandsActive(false); + // Class under test s = new Store(addon); } @@ -281,4 +297,130 @@ public void testSaveOnlinePlayers() { } } + // --- Per-island storage key tests --- + + @Test + public void testGetStorageKeyIslandsDisabled() { + sets.setIslandsActive(false); + String key = s.getStorageKey(player, world); + assertEquals("world", key); // nether suffix stripped + } + + @Test + public void testGetStorageKeySingleIsland() { + sets.setIslandsActive(true); + try (MockedStatic utilities = Mockito.mockStatic(Util.class)) { + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(1); + String key = s.getStorageKey(player, world); + assertEquals("world", key); // just overworld name, no island suffix + } + } + + @Test + public void testGetStorageKeyMultipleIslandsOnOwnIsland() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + when(island.getUniqueId()).thenReturn("island-123"); + Location loc = mock(Location.class); + when(player.getLocation()).thenReturn(loc); + + try (MockedStatic utilities = Mockito.mockStatic(Util.class)) { + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + when(islandsManager.getIslandAt(loc)).thenReturn(Optional.of(island)); + + String key = s.getStorageKey(player, world); + assertEquals("world/island-123", key); + } + } + + @Test + public void testGetStorageKeyMultipleIslandsOnOtherPlayerIsland() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + UUID otherPlayer = UUID.randomUUID(); + when(island.getOwner()).thenReturn(otherPlayer); + Location loc = mock(Location.class); + when(player.getLocation()).thenReturn(loc); + + try (MockedStatic utilities = Mockito.mockStatic(Util.class)) { + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + when(islandsManager.getIslandAt(loc)).thenReturn(Optional.of(island)); + + String key = s.getStorageKey(player, world); + // Not on own island, falls back to overworld name + assertEquals("world", key); + } + } + + @Test + public void testGetStorageKeyWithSpecificIsland() { + sets.setIslandsActive(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + when(island.getUniqueId()).thenReturn("island-456"); + + try (MockedStatic utilities = Mockito.mockStatic(Util.class)) { + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + + String key = s.getStorageKey(player, world, island); + assertEquals("world/island-456", key); + } + } + + @Test + public void testGetStorageKeyGenericNether() { + sets.setIslandsActive(true); + // Set up a nether world + World netherWorld = mock(World.class); + when(netherWorld.getName()).thenReturn("world_nether"); + when(netherWorld.getEnvironment()).thenReturn(Environment.NETHER); + + // Set up overworld for Util.getWorld + World overworld = mock(World.class); + when(overworld.getName()).thenReturn("world"); + + // Set up BentoBox IWM + BentoBox plugin = mock(BentoBox.class); + IslandWorldManager iwm = mock(IslandWorldManager.class); + when(plugin.getIWM()).thenReturn(iwm); + when(iwm.isIslandNether(netherWorld)).thenReturn(false); // generic nether + + try (MockedStatic utilities = Mockito.mockStatic(Util.class); + MockedStatic bentoBoxStatic = mockStatic(BentoBox.class)) { + utilities.when(() -> Util.getWorld(netherWorld)).thenReturn(overworld); + bentoBoxStatic.when(BentoBox::getInstance).thenReturn(plugin); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, overworld)).thenReturn(2); + + // No current key set yet, should fall back to overworld name + String key = s.getStorageKey(player, netherWorld); + assertEquals("world", key); + } + } + + @Test + public void testGetCurrentKeyNullByDefault() { + assertNull(s.getCurrentKey(player)); + } + + @Test + public void testGetCurrentKeySetAfterGetInventory() { + sets.setIslandsActive(false); + s.getInventory(player, world); + assertEquals("world", s.getCurrentKey(player)); + } + + @Test + public void testRemoveFromCacheClearsCurrentKey() { + sets.setIslandsActive(false); + s.getInventory(player, world); + assertNotNull(s.getCurrentKey(player)); + s.removeFromCache(player); + assertNull(s.getCurrentKey(player)); + } + } diff --git a/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java b/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java index 36c67ea..fd4121a 100644 --- a/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java +++ b/src/test/java/com/wasteofplastic/invswitcher/listeners/PlayerListenerTest.java @@ -2,18 +2,24 @@ import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Optional; import java.util.Set; +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerChangedWorldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerRespawnEvent; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,15 +29,19 @@ import org.mockito.junit.MockitoJUnitRunner; import com.wasteofplastic.invswitcher.InvSwitcher; +import com.wasteofplastic.invswitcher.Settings; import com.wasteofplastic.invswitcher.Store; +import world.bentobox.bentobox.api.events.island.IslandEnterEvent; +import world.bentobox.bentobox.database.objects.Island; +import world.bentobox.bentobox.managers.IslandsManager; import world.bentobox.bentobox.util.Util; /** * @author tastybento * */ -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class PlayerListenerTest { @Mock @@ -45,11 +55,19 @@ public class PlayerListenerTest { private World world; @Mock private World notWorld; + @Mock + private Settings settings; + @Mock + private IslandsManager islandsManager; + + private UUID playerUUID; /** */ @Before public void setUp() { + playerUUID = UUID.randomUUID(); + when(player.getUniqueId()).thenReturn(playerUUID); // Util // Mock the static method try (MockedStatic mockedBukkit = mockStatic(Util.class, Mockito.RETURNS_MOCKS)) { @@ -61,6 +79,8 @@ public void setUp() { // Addon when(addon.getStore()).thenReturn(store); when(addon.getWorlds()).thenReturn(Set.of(world)); + when(addon.getSettings()).thenReturn(settings); + when(addon.getIslands()).thenReturn(islandsManager); pl = new PlayerListener(addon); } @@ -157,4 +177,155 @@ public void testOnPlayerQuitNotCoveredWorld() { verify(store).removeFromCache(player); } + // --- Island Enter Event Tests --- + + @Test + public void testOnIslandEnterDisabled() { + when(settings.isIslandsActive()).thenReturn(false); + Island island = mock(Island.class); + IslandEnterEvent event = new IslandEnterEvent(island, playerUUID, false, null, island, null); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + pl.onIslandEnter(event); + } + verify(store, never()).storeInventory(any(), any()); + } + + @Test + public void testOnIslandEnterNotOwner() { + when(settings.isIslandsActive()).thenReturn(true); + Island island = mock(Island.class); + UUID otherOwner = UUID.randomUUID(); + when(island.getOwner()).thenReturn(otherOwner); + IslandEnterEvent event = new IslandEnterEvent(island, playerUUID, false, null, island, null); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + pl.onIslandEnter(event); + } + verify(store, never()).storeInventory(any(), any()); + } + + @Test + public void testOnIslandEnterSingleIsland() { + when(settings.isIslandsActive()).thenReturn(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + + IslandEnterEvent event = new IslandEnterEvent(island, playerUUID, false, null, island, null); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic utilities = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(1); + pl.onIslandEnter(event); + } + verify(store, never()).storeInventory(any(), any()); + } + + @Test + public void testOnIslandEnterMultipleIslandsSameKey() { + when(settings.isIslandsActive()).thenReturn(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + when(island.getUniqueId()).thenReturn("island-1"); + + // Current key already matches + when(store.getStorageKey(player, world, island)).thenReturn("world/island-1"); + when(store.getCurrentKey(player)).thenReturn("world/island-1"); + + IslandEnterEvent event = new IslandEnterEvent(island, playerUUID, false, null, island, null); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic utilities = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + pl.onIslandEnter(event); + } + // Same key, no switch + verify(store, never()).storeInventory(any(), any()); + } + + @Test + public void testOnIslandEnterMultipleIslandsDifferentKey() { + when(settings.isIslandsActive()).thenReturn(true); + Island island = mock(Island.class); + when(island.getOwner()).thenReturn(playerUUID); + when(island.getUniqueId()).thenReturn("island-2"); + + when(store.getStorageKey(player, world, island)).thenReturn("world/island-2"); + when(store.getCurrentKey(player)).thenReturn("world/island-1"); + + IslandEnterEvent event = new IslandEnterEvent(island, playerUUID, false, null, island, null); + try (MockedStatic mockedBukkit = mockStatic(Bukkit.class); + MockedStatic utilities = mockStatic(Util.class)) { + mockedBukkit.when(() -> Bukkit.getPlayer(playerUUID)).thenReturn(player); + utilities.when(() -> Util.getWorld(world)).thenReturn(world); + when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2); + pl.onIslandEnter(event); + } + // Different key, switch should happen + verify(store).storeInventory(player, world); + verify(store).getInventory(player, world, island); + } + + // --- Respawn Event Tests --- + + @Test + public void testOnPlayerRespawnDisabled() { + when(settings.isIslandsActive()).thenReturn(false); + Location respawnLoc = mock(Location.class); + when(respawnLoc.getWorld()).thenReturn(world); + PlayerRespawnEvent event = new PlayerRespawnEvent(player, respawnLoc, false); + pl.onPlayerRespawn(event); + verify(store, never()).storeAndSave(any(), any(), any(boolean.class)); + } + + @Test + public void testOnPlayerRespawnSameIsland() { + when(settings.isIslandsActive()).thenReturn(true); + Location respawnLoc = mock(Location.class); + when(respawnLoc.getWorld()).thenReturn(world); + + Island island = mock(Island.class); + when(islandsManager.getIslandAt(respawnLoc)).thenReturn(Optional.of(island)); + when(store.getStorageKey(player, world, island)).thenReturn("world/island-1"); + when(store.getCurrentKey(player)).thenReturn("world/island-1"); + + PlayerRespawnEvent event = new PlayerRespawnEvent(player, respawnLoc, false); + pl.onPlayerRespawn(event); + // Same island, no switch + verify(store, never()).storeAndSave(any(), any(), any(boolean.class)); + } + + @Test + public void testOnPlayerRespawnDifferentIsland() { + when(settings.isIslandsActive()).thenReturn(true); + Location respawnLoc = mock(Location.class); + when(respawnLoc.getWorld()).thenReturn(world); + + Island island = mock(Island.class); + when(islandsManager.getIslandAt(respawnLoc)).thenReturn(Optional.of(island)); + when(store.getStorageKey(player, world, island)).thenReturn("world/island-2"); + when(store.getCurrentKey(player)).thenReturn("world/island-1"); + + PlayerRespawnEvent event = new PlayerRespawnEvent(player, respawnLoc, false); + pl.onPlayerRespawn(event); + // Different island, switch should happen + verify(store).storeAndSave(player, world, false); + verify(store).getInventory(player, world, island); + } + + @Test + public void testOnPlayerRespawnNoIsland() { + when(settings.isIslandsActive()).thenReturn(true); + Location respawnLoc = mock(Location.class); + when(respawnLoc.getWorld()).thenReturn(world); + when(islandsManager.getIslandAt(respawnLoc)).thenReturn(Optional.empty()); + + PlayerRespawnEvent event = new PlayerRespawnEvent(player, respawnLoc, false); + pl.onPlayerRespawn(event); + // No island at respawn, no switch + verify(store, never()).storeAndSave(any(), any(), any(boolean.class)); + } + }