money) {
+ this.money = money;
+ }
+
+ /**
+ * Gets the money balance for a specific key.
+ * @param key the world/island key
+ * @return the balance, or null if no balance has been stored for this key
+ */
+ public Double getMoney(String key) {
+ return money == null ? null : money.get(key);
+ }
+
+ /**
+ * Checks whether a balance has been stored for a specific key.
+ * @param key the world/island key
+ * @return true if a balance exists for this key
+ */
+ public boolean hasMoney(String key) {
+ return money != null && money.containsKey(key);
+ }
+
+ /**
+ * Sets the money balance for a specific key.
+ * @param key the world/island key
+ * @param balance the balance to set
+ */
+ public void setMoney(String key, double balance) {
+ if (this.money == null) {
+ this.money = new HashMap<>();
+ }
+ this.money.put(key, balance);
+ }
+
+ /**
+ * Gets the last storage key the player was tracked under.
+ * @return the last key, or null if never set
+ */
+ public String getLastKey() {
+ return lastKey;
+ }
+
+ /**
+ * Sets the last storage key the player was tracked under.
+ * @param lastKey the last key to set
+ */
+ public void setLastKey(String lastKey) {
+ this.lastKey = lastKey;
+ }
+
+ /**
+ * @return whether this player's balance has already been imported
+ */
+ public boolean isImported() {
+ return imported;
+ }
+
+ /**
+ * @param imported whether this player's balance has been imported
+ */
+ public void setImported(boolean imported) {
+ this.imported = imported;
+ }
+
/**
* Sets the location for a specific world.
* @param worldName the world name
@@ -437,6 +533,9 @@ public void clearWorldData(String worldName) {
this.gameMode.remove(worldName);
this.advancements.remove(worldName);
this.enderChest.remove(worldName);
+ if (this.money != null) {
+ this.money.remove(worldName);
+ }
clearStats(worldName);
}
diff --git a/src/main/java/com/wasteofplastic/invswitcher/economy/InvEconomy.java b/src/main/java/com/wasteofplastic/invswitcher/economy/InvEconomy.java
new file mode 100644
index 0000000..b786ee4
--- /dev/null
+++ b/src/main/java/com/wasteofplastic/invswitcher/economy/InvEconomy.java
@@ -0,0 +1,531 @@
+package com.wasteofplastic.invswitcher.economy;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.plugin.RegisteredServiceProvider;
+
+import com.wasteofplastic.invswitcher.InvSwitcher;
+import com.wasteofplastic.invswitcher.Settings;
+import com.wasteofplastic.invswitcher.Store;
+import com.wasteofplastic.invswitcher.dataobjects.InventoryStorage;
+
+import net.milkbowl.vault.economy.Economy;
+import net.milkbowl.vault.economy.EconomyResponse;
+import net.milkbowl.vault.economy.EconomyResponse.ResponseType;
+
+/**
+ * Vault {@link Economy} implementation that gives every InvSwitcher-managed world its own
+ * balance. InvSwitcher registers this provider at the highest priority so it intercepts every
+ * economy call server-wide and routes it to the correct world's balance — even when the
+ * target player is offline or in a different world.
+ *
+ * Worlds that InvSwitcher does not manage are passed through to the economy provider that was
+ * registered before InvSwitcher (the {@code delegate}, e.g. EssentialsX), so the rest of the
+ * server's economy is unaffected.
+ *
+ * @author tastybento
+ */
+public class InvEconomy implements Economy {
+
+ private static final String NEGATIVE_DEPOSIT = "Cannot deposit a negative amount";
+ private static final String NEGATIVE_WITHDRAW = "Cannot withdraw a negative amount";
+ private static final String NEGATIVE_SET = "Cannot set a negative balance";
+ private static final String INSUFFICIENT_FUNDS = "Insufficient funds";
+
+ private final InvSwitcher addon;
+ /** The economy to fall back to for unmanaged worlds (e.g. EssentialsX). Resolved lazily on
+ * first use, because we register before other economy plugins finish registering. */
+ private Economy delegate;
+ private boolean delegateResolved;
+
+ /**
+ * @param addon - the InvSwitcher addon
+ */
+ public InvEconomy(InvSwitcher addon) {
+ this.addon = addon;
+ }
+
+ /**
+ * Package-private constructor that sets the delegate explicitly instead of resolving it
+ * lazily from the services manager. Used by tests.
+ * @param addon - the InvSwitcher addon
+ * @param delegate - the delegate economy (may be null)
+ */
+ InvEconomy(InvSwitcher addon, Economy delegate) {
+ this.addon = addon;
+ this.delegate = delegate;
+ this.delegateResolved = true;
+ }
+
+ private Settings settings() {
+ return addon.getSettings();
+ }
+
+ /**
+ * The store, fetched lazily. May be null very early in startup (before allLoaded), but is
+ * always set long before any player can trade.
+ */
+ private Store store() {
+ return addon.getStore();
+ }
+
+ /**
+ * The player's current money key, or null if the world is unmanaged or the store is not yet
+ * ready (very early in startup). A null result routes the call to the delegate.
+ */
+ private String currentKey(OfflinePlayer player) {
+ Store s = store();
+ return s == null ? null : s.getCurrentMoneyKey(player);
+ }
+
+ /**
+ * The player's money key for a named world, or null if unmanaged / store not ready.
+ */
+ private String worldKey(OfflinePlayer player, String worldName) {
+ Store s = store();
+ return s == null ? null : s.getMoneyKey(player, Bukkit.getWorld(worldName));
+ }
+
+ /**
+ * Resolve - once, lazily - the highest-priority economy that is not us, to delegate
+ * unmanaged-world transactions to. Done lazily because when we register (during onEnable)
+ * other economy plugins such as EssentialsX may not have registered their provider yet.
+ * @return the delegate economy, or null if none exists
+ */
+ private Economy delegate() {
+ if (!delegateResolved) {
+ Economy found = null;
+ for (RegisteredServiceProvider r : Bukkit.getServicesManager()
+ .getRegistrations(Economy.class)) {
+ if (!(r.getProvider() instanceof InvEconomy)) {
+ found = r.getProvider();
+ break;
+ }
+ }
+ delegate = found;
+ delegateResolved = true;
+ debug(found != null
+ ? "delegating unmanaged-world transactions to " + found.getName()
+ : "no other economy found; InvSwitcher is the only economy.");
+ }
+ return delegate;
+ }
+
+ private void debug(String msg) {
+ if (settings().isEconomyDebug()) {
+ addon.log("[economy] " + msg);
+ }
+ }
+
+ private static String who(OfflinePlayer player) {
+ String name = player.getName() != null ? player.getName() : player.getUniqueId().toString();
+ return name + (player.getPlayer() != null ? " (online)" : " (offline)");
+ }
+
+ /**
+ * @return true if unmanaged worlds should be delegated to the previous provider
+ */
+ private boolean delegating() {
+ return settings().isDelegateUnmanagedWorlds() && delegate() != null;
+ }
+
+ // ------ STATUS / FORMATTING ------
+
+ @Override
+ public boolean isEnabled() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "InvSwitcher";
+ }
+
+ @Override
+ public boolean hasBankSupport() {
+ return delegate() != null && delegate().hasBankSupport();
+ }
+
+ @Override
+ public int fractionalDigits() {
+ return settings().getFractionalDigits();
+ }
+
+ @Override
+ public String format(double amount) {
+ String name = Math.abs(amount - 1.0D) < 1.0E-9D ? currencyNameSingular() : currencyNamePlural();
+ return String.format("%,." + Math.max(0, fractionalDigits()) + "f %s", amount, name);
+ }
+
+ @Override
+ public String currencyNamePlural() {
+ return settings().getCurrencyNamePlural();
+ }
+
+ @Override
+ public String currencyNameSingular() {
+ return settings().getCurrencyNameSingular();
+ }
+
+ // ------ CORE BALANCE LOGIC (operates on a single storage object per call) ------
+
+ /**
+ * Ensure the key has a balance, seeding it (via import or starting balance) if absent.
+ * Mutates {@code s} in memory only; the caller is responsible for persisting.
+ */
+ private double ensureBalance(OfflinePlayer player, InventoryStorage s, String key) {
+ if (!s.hasMoney(key)) {
+ s.setMoney(key, seedBalance(player, s));
+ }
+ return s.getMoney(key);
+ }
+
+ /**
+ * Determine the initial balance for a player's first-ever managed-world key: either their
+ * existing balance imported once from the previous economy, or the configured starting
+ * balance.
+ */
+ private double seedBalance(OfflinePlayer player, InventoryStorage s) {
+ Settings settings = settings();
+ if (settings.isImportExistingBalances() && delegate() != null && !s.isImported()) {
+ s.setImported(true);
+ return delegate().getBalance(player);
+ }
+ return settings.getStartingBalance();
+ }
+
+ private double readSelf(OfflinePlayer player, String key) {
+ InventoryStorage s = store().getStorageObject(player.getUniqueId());
+ boolean existed = s.hasMoney(key);
+ double balance = ensureBalance(player, s, key);
+ if (!existed) {
+ store().saveStorage(s);
+ }
+ return balance;
+ }
+
+ private EconomyResponse depositSelf(OfflinePlayer player, String key, double amount) {
+ if (amount < 0) {
+ return new EconomyResponse(0, readSelf(player, key), ResponseType.FAILURE, NEGATIVE_DEPOSIT);
+ }
+ InventoryStorage s = store().getStorageObject(player.getUniqueId());
+ double newBalance = ensureBalance(player, s, key) + amount;
+ s.setMoney(key, newBalance);
+ store().saveStorage(s);
+ return new EconomyResponse(amount, newBalance, ResponseType.SUCCESS, null);
+ }
+
+ private EconomyResponse setSelf(OfflinePlayer player, String key, double amount) {
+ if (amount < 0) {
+ return new EconomyResponse(0, readSelf(player, key), ResponseType.FAILURE, NEGATIVE_SET);
+ }
+ InventoryStorage s = store().getStorageObject(player.getUniqueId());
+ // Mark imported so a later seed does not re-import on top of an explicitly set balance
+ s.setImported(true);
+ s.setMoney(key, amount);
+ store().saveStorage(s);
+ return new EconomyResponse(amount, amount, ResponseType.SUCCESS, null);
+ }
+
+ private EconomyResponse withdrawSelf(OfflinePlayer player, String key, double amount) {
+ if (amount < 0) {
+ return new EconomyResponse(0, readSelf(player, key), ResponseType.FAILURE, NEGATIVE_WITHDRAW);
+ }
+ InventoryStorage s = store().getStorageObject(player.getUniqueId());
+ double balance = ensureBalance(player, s, key);
+ if (balance < amount) {
+ return new EconomyResponse(0, balance, ResponseType.FAILURE, INSUFFICIENT_FUNDS);
+ }
+ double newBalance = balance - amount;
+ s.setMoney(key, newBalance);
+ store().saveStorage(s);
+ return new EconomyResponse(amount, newBalance, ResponseType.SUCCESS, null);
+ }
+
+ // ------ ACCOUNT METHODS (OfflinePlayer) ------
+
+ @Override
+ public boolean hasAccount(OfflinePlayer player) {
+ return true;
+ }
+
+ @Override
+ public boolean hasAccount(OfflinePlayer player, String worldName) {
+ return true;
+ }
+
+ @Override
+ public double getBalance(OfflinePlayer player) {
+ String key = currentKey(player);
+ debug("getBalance " + who(player) + " key=" + key + (key == null ? (delegating() ? " -> DELEGATE" : " -> default") : ""));
+ if (key == null) {
+ return delegating() ? delegate().getBalance(player) : readSelf(player, Store.DEFAULT_WORLD_KEY);
+ }
+ return readSelf(player, key);
+ }
+
+ @Override
+ public double getBalance(OfflinePlayer player, String worldName) {
+ String key = worldKey(player, worldName);
+ if (key == null) {
+ return delegating() ? delegate().getBalance(player, worldName) : readSelf(player, Store.DEFAULT_WORLD_KEY);
+ }
+ return readSelf(player, key);
+ }
+
+ @Override
+ public boolean has(OfflinePlayer player, double amount) {
+ return getBalance(player) >= amount;
+ }
+
+ @Override
+ public boolean has(OfflinePlayer player, String worldName, double amount) {
+ return getBalance(player, worldName) >= amount;
+ }
+
+ @Override
+ public EconomyResponse withdrawPlayer(OfflinePlayer player, double amount) {
+ String key = currentKey(player);
+ debug("withdrawPlayer " + who(player) + " amount=" + amount + " key=" + key
+ + (key == null ? (delegating() ? " -> DELEGATE" : " -> default") : " -> self"));
+ if (key == null) {
+ return delegating() ? delegate().withdrawPlayer(player, amount)
+ : withdrawSelf(player, Store.DEFAULT_WORLD_KEY, amount);
+ }
+ return withdrawSelf(player, key, amount);
+ }
+
+ @Override
+ public EconomyResponse withdrawPlayer(OfflinePlayer player, String worldName, double amount) {
+ String key = worldKey(player, worldName);
+ debug("withdrawPlayer(world) " + who(player) + " world=" + worldName + " amount=" + amount + " key=" + key
+ + (key == null ? (delegating() ? " -> DELEGATE" : " -> default") : " -> self"));
+ if (key == null) {
+ return delegating() ? delegate().withdrawPlayer(player, worldName, amount)
+ : withdrawSelf(player, Store.DEFAULT_WORLD_KEY, amount);
+ }
+ return withdrawSelf(player, key, amount);
+ }
+
+ @Override
+ public EconomyResponse depositPlayer(OfflinePlayer player, double amount) {
+ String key = currentKey(player);
+ debug("depositPlayer " + who(player) + " amount=" + amount + " key=" + key
+ + (key == null ? (delegating() ? " -> DELEGATE" : " -> default") : " -> self"));
+ if (key == null) {
+ return delegating() ? delegate().depositPlayer(player, amount)
+ : depositSelf(player, Store.DEFAULT_WORLD_KEY, amount);
+ }
+ return depositSelf(player, key, amount);
+ }
+
+ @Override
+ public EconomyResponse depositPlayer(OfflinePlayer player, String worldName, double amount) {
+ String key = worldKey(player, worldName);
+ debug("depositPlayer(world) " + who(player) + " world=" + worldName + " amount=" + amount + " key=" + key
+ + (key == null ? (delegating() ? " -> DELEGATE" : " -> default") : " -> self"));
+ if (key == null) {
+ return delegating() ? delegate().depositPlayer(player, worldName, amount)
+ : depositSelf(player, Store.DEFAULT_WORLD_KEY, amount);
+ }
+ return depositSelf(player, key, amount);
+ }
+
+ /**
+ * Set a player's balance for their current (online) or last-known (offline) world. Not part
+ * of the Vault interface; used by InvSwitcher's own admin commands.
+ * @param player - player
+ * @param amount - new balance
+ * @return the economy response
+ */
+ public EconomyResponse setBalance(OfflinePlayer player, double amount) {
+ String key = currentKey(player);
+ if (key == null) {
+ if (delegating()) {
+ // Vault has no set operation; emulate it against the delegate's balance
+ double delta = amount - delegate().getBalance(player);
+ return delta >= 0 ? delegate().depositPlayer(player, delta) : delegate().withdrawPlayer(player, -delta);
+ }
+ key = Store.DEFAULT_WORLD_KEY;
+ }
+ return setSelf(player, key, amount);
+ }
+
+ /**
+ * Set a player's balance for a specific world. Not part of the Vault interface; used by
+ * InvSwitcher's own admin commands so that, e.g., {@code /bsb eco set} targets the BSkyBlock
+ * balance regardless of where the player or admin is standing.
+ * @param player - player
+ * @param worldName - world to set the balance in
+ * @param amount - new balance
+ * @return the economy response
+ */
+ public EconomyResponse setBalance(OfflinePlayer player, String worldName, double amount) {
+ String key = worldKey(player, worldName);
+ if (key == null) {
+ if (delegating()) {
+ double delta = amount - delegate().getBalance(player, worldName);
+ return delta >= 0 ? delegate().depositPlayer(player, worldName, delta)
+ : delegate().withdrawPlayer(player, worldName, -delta);
+ }
+ key = Store.DEFAULT_WORLD_KEY;
+ }
+ return setSelf(player, key, amount);
+ }
+
+ @Override
+ public boolean createPlayerAccount(OfflinePlayer player) {
+ return true;
+ }
+
+ @Override
+ public boolean createPlayerAccount(OfflinePlayer player, String worldName) {
+ return true;
+ }
+
+ // ------ DEPRECATED NAME-BASED METHODS (delegate up to OfflinePlayer variants) ------
+
+ @Override
+ @Deprecated
+ public boolean hasAccount(String playerName) {
+ return hasAccount(Bukkit.getOfflinePlayer(playerName));
+ }
+
+ @Override
+ @Deprecated
+ public boolean hasAccount(String playerName, String worldName) {
+ return hasAccount(Bukkit.getOfflinePlayer(playerName), worldName);
+ }
+
+ @Override
+ @Deprecated
+ public double getBalance(String playerName) {
+ return getBalance(Bukkit.getOfflinePlayer(playerName));
+ }
+
+ @Override
+ @Deprecated
+ public double getBalance(String playerName, String world) {
+ return getBalance(Bukkit.getOfflinePlayer(playerName), world);
+ }
+
+ @Override
+ @Deprecated
+ public boolean has(String playerName, double amount) {
+ return has(Bukkit.getOfflinePlayer(playerName), amount);
+ }
+
+ @Override
+ @Deprecated
+ public boolean has(String playerName, String worldName, double amount) {
+ return has(Bukkit.getOfflinePlayer(playerName), worldName, amount);
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse withdrawPlayer(String playerName, double amount) {
+ return withdrawPlayer(Bukkit.getOfflinePlayer(playerName), amount);
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse withdrawPlayer(String playerName, String worldName, double amount) {
+ return withdrawPlayer(Bukkit.getOfflinePlayer(playerName), worldName, amount);
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse depositPlayer(String playerName, double amount) {
+ return depositPlayer(Bukkit.getOfflinePlayer(playerName), amount);
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse depositPlayer(String playerName, String worldName, double amount) {
+ return depositPlayer(Bukkit.getOfflinePlayer(playerName), worldName, amount);
+ }
+
+ @Override
+ @Deprecated
+ public boolean createPlayerAccount(String playerName) {
+ return createPlayerAccount(Bukkit.getOfflinePlayer(playerName));
+ }
+
+ @Override
+ @Deprecated
+ public boolean createPlayerAccount(String playerName, String worldName) {
+ return createPlayerAccount(Bukkit.getOfflinePlayer(playerName), worldName);
+ }
+
+ // ------ BANK METHODS (passed through to the delegate; not partitioned per world) ------
+
+ private EconomyResponse bankUnsupported() {
+ return new EconomyResponse(0, 0, ResponseType.NOT_IMPLEMENTED, "InvSwitcher does not support banks");
+ }
+
+ @Override
+ public EconomyResponse createBank(String name, OfflinePlayer player) {
+ return delegate() != null ? delegate().createBank(name, player) : bankUnsupported();
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse createBank(String name, String player) {
+ return delegate() != null ? delegate().createBank(name, player) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse deleteBank(String name) {
+ return delegate() != null ? delegate().deleteBank(name) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse bankBalance(String name) {
+ return delegate() != null ? delegate().bankBalance(name) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse bankHas(String name, double amount) {
+ return delegate() != null ? delegate().bankHas(name, amount) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse bankWithdraw(String name, double amount) {
+ return delegate() != null ? delegate().bankWithdraw(name, amount) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse bankDeposit(String name, double amount) {
+ return delegate() != null ? delegate().bankDeposit(name, amount) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse isBankOwner(String name, OfflinePlayer player) {
+ return delegate() != null ? delegate().isBankOwner(name, player) : bankUnsupported();
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse isBankOwner(String name, String playerName) {
+ return delegate() != null ? delegate().isBankOwner(name, playerName) : bankUnsupported();
+ }
+
+ @Override
+ public EconomyResponse isBankMember(String name, OfflinePlayer player) {
+ return delegate() != null ? delegate().isBankMember(name, player) : bankUnsupported();
+ }
+
+ @Override
+ @Deprecated
+ public EconomyResponse isBankMember(String name, String playerName) {
+ return delegate() != null ? delegate().isBankMember(name, playerName) : bankUnsupported();
+ }
+
+ @Override
+ public List getBanks() {
+ return delegate() != null ? delegate().getBanks() : Collections.emptyList();
+ }
+}
diff --git a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java
index 97110c5..d81e119 100644
--- a/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java
+++ b/src/main/java/com/wasteofplastic/invswitcher/listeners/PlayerListener.java
@@ -17,6 +17,13 @@
import com.wasteofplastic.invswitcher.InvSwitcher;
import world.bentobox.bentobox.api.events.island.IslandEnterEvent;
+import world.bentobox.bentobox.api.events.player.PlayerBaseEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetEnderChestEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetExpEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetHealthEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetHungerEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetInventoryEvent;
+import world.bentobox.bentobox.api.events.player.PlayerResetMoneyEvent;
import world.bentobox.bentobox.database.objects.Island;
import world.bentobox.bentobox.util.Util;
@@ -183,5 +190,133 @@ public void onPlayerQuit(final PlayerQuitEvent event) {
addon.getStore().removeFromCache(event.getPlayer());
}
+ /**
+ * Intercepts BentoBox's inventory reset when the player is not in the BentoBox world.
+ * Cancels the direct clear and instead wipes the stored inventory data for that world.
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetInventory(PlayerResetInventoryEvent event) {
+ // If we are not switching inventories, let BentoBox perform its own reset.
+ if (!addon.getSettings().isInventory()) return;
+ if (!shouldInterceptPlayerReset(event)) return;
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredInventoryForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Intercepts BentoBox's ender chest reset when the player is not in the BentoBox world.
+ * Cancels the direct clear and instead wipes the stored ender chest data for that world.
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetEnderChest(PlayerResetEnderChestEvent event) {
+ // If we are not switching ender chests, let BentoBox perform its own reset.
+ if (!addon.getSettings().isEnderChest()) return;
+ if (!shouldInterceptPlayerReset(event)) return;
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredEnderChestForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Intercepts BentoBox's experience reset when the player is not in the BentoBox world.
+ * Cancels the direct clear and instead zeroes the stored experience for that world.
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetExp(PlayerResetExpEvent event) {
+ // If we are not switching experience, let BentoBox perform its own reset.
+ if (!addon.getSettings().isExperience()) return;
+ if (!shouldInterceptPlayerReset(event)) return;
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredExpForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Intercepts BentoBox's health reset when the player is not in the BentoBox world.
+ * Cancels the direct reset and instead removes the stored health for that world
+ * (so the player receives max health when they next enter the world).
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetHealth(PlayerResetHealthEvent event) {
+ // If we are not switching health, let BentoBox perform its own reset.
+ if (!addon.getSettings().isHealth()) return;
+ if (!shouldInterceptPlayerReset(event)) return;
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredHealthForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Intercepts BentoBox's hunger reset when the player is not in the BentoBox world.
+ * Cancels the direct reset and instead sets stored food to full (20) for that world.
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetHunger(PlayerResetHungerEvent event) {
+ // If we are not switching food, let BentoBox perform its own reset.
+ if (!addon.getSettings().isFood()) return;
+ if (!shouldInterceptPlayerReset(event)) return;
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredFoodForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Intercepts BentoBox's money reset when the player is not in the BentoBox world. BentoBox's
+ * default reset reads the player's current world balance and withdraws it from the
+ * event world, which is wrong when the player is elsewhere. Instead, cancel it and zero the
+ * stored balance for the event world directly. When the player is in the event world the
+ * reset is left to BentoBox, which routes correctly through InvSwitcher's economy.
+ * @param event - event
+ */
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
+ public void onPlayerResetMoney(PlayerResetMoneyEvent event) {
+ if (!addon.getSettings().isMoney()) {
+ return;
+ }
+ if (!shouldInterceptPlayerReset(event)) {
+ return;
+ }
+ event.setCancelled(true);
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player != null) {
+ addon.getStore().clearStoredMoneyForWorld(player, event.getWorld(), event.getIsland());
+ }
+ }
+
+ /**
+ * Determines whether InvSwitcher should intercept a BentoBox player reset event.
+ * Returns true if the event's world is managed by InvSwitcher, the player is online,
+ * and the player is currently in a different world (not the event world).
+ * @param event - the reset event
+ * @return true if InvSwitcher should cancel the event and handle it itself
+ */
+ private boolean shouldInterceptPlayerReset(PlayerBaseEvent event) {
+ World eventWorld = event.getWorld();
+ if (!addon.getWorlds().contains(eventWorld)) {
+ return false;
+ }
+ Player player = Bukkit.getPlayer(event.getPlayerUUID());
+ if (player == null) {
+ return false;
+ }
+ // Only intercept if the player is not currently in the event world
+ return !Util.sameWorld(player.getWorld(), eventWorld);
+ }
}
diff --git a/src/main/resources/addon.yml b/src/main/resources/addon.yml
index 7eba1d8..5cfe563 100755
--- a/src/main/resources/addon.yml
+++ b/src/main/resources/addon.yml
@@ -1,7 +1,7 @@
name: InvSwitcher
main: com.wasteofplastic.invswitcher.InvSwitcher
version: ${version}${build.number}
-api-version: 3.0.0
+api-version: 3.17.0
authors: tastybento
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 34a19b8..e04efdd 100755
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -20,6 +20,13 @@ options:
experience: true
ender-chest: true
statistics: true
+ # Per-world money. Requires the Vault plugin. A separate economy plugin (e.g. EssentialsX) is
+ # optional: InvSwitcher can be the only economy. When enabled, InvSwitcher registers itself as
+ # the Vault economy and keeps a separate balance for each switched world. Transactions route to
+ # the correct world even when the player is offline or in a different world. If another economy
+ # plugin is present, worlds InvSwitcher does not manage are passed through to it; if not,
+ # InvSwitcher handles every world itself.
+ money: 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.
@@ -33,3 +40,24 @@ options:
experience: false
ender-chest: true
statistics: false
+ # If true, each of a player's islands has its own wallet. If false (default) money is
+ # per-world only. Per-island money is best-effort for offline players.
+ money: false
+# Economy settings. Only used when options.money is true.
+economy:
+ # Balance given to a player the first time they enter a managed world (unless imported).
+ starting-balance: 0.0
+ # Currency names used when formatting amounts.
+ currency-name-singular: Dollar
+ currency-name-plural: Dollars
+ # Number of digits after the decimal point.
+ fractional-digits: 2
+ # Import each player's existing balance from the previous economy plugin once, the first
+ # time they enter a managed world, so nobody loses money on first run.
+ import-existing-balances: true
+ # Pass transactions for worlds InvSwitcher does not manage through to the previous economy
+ # plugin (e.g. EssentialsX). Disable to make InvSwitcher the sole economy for every world.
+ delegate-unmanaged-worlds: true
+ # Log every economy transaction (deposit/withdraw/balance) to the console for troubleshooting.
+ # Verbose - only enable while diagnosing a problem.
+ debug: false
diff --git a/src/main/resources/locales/cs.yml b/src/main/resources/locales/cs.yml
new file mode 100644
index 0000000..565a8fb
--- /dev/null
+++ b/src/main/resources/locales/cs.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Czech
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "zobrazí tvůj zůstatek pro tento svět"
+ balance: "&a Zůstatek: &e[number]"
+ pay:
+ description: "zaplatit jinému hráči"
+ parameters: " "
+ sent: "&a Zaplatil jsi [number] hráči [name]"
+ received: "&a Obdržel jsi [number] od hráče [name]"
+ admin:
+ eco:
+ description: "spravovat peníze hráčů v tomto světě"
+ give:
+ description: "dát hráči peníze"
+ parameters: " "
+ success: "&a Peníze předány hráči [name]. Nový zůstatek: [number]"
+ take:
+ description: "vzít hráči peníze"
+ parameters: " "
+ success: "&a Peníze odebrány hráči [name]. Nový zůstatek: [number]"
+ set:
+ description: "nastavit zůstatek hráče"
+ parameters: " "
+ success: "&a Zůstatek hráče [name] nastaven na [number]"
+ balance:
+ description: "zobrazit zůstatek hráče"
+ parameters: ""
+ balance: "&a Zůstatek hráče [name]: &e[number]"
+ errors:
+ no-economy: "&c Ekonomika není dostupná."
+ insufficient-funds: "&c Nedostatek peněz."
+ not-a-number: "&c '[number]' není platné číslo."
+ must-be-positive: "&c Částka musí být kladná."
+ cannot-pay-self: "&c Nemůžeš zaplatit sám sobě."
diff --git a/src/main/resources/locales/de.yml b/src/main/resources/locales/de.yml
new file mode 100644
index 0000000..ad4d8fc
--- /dev/null
+++ b/src/main/resources/locales/de.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - German
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "zeige deinen Kontostand für diese Welt"
+ balance: "&a Kontostand: &e[number]"
+ pay:
+ description: "zahle einem anderen Spieler Geld"
+ parameters: " "
+ sent: "&a Du hast [number] an [name] gezahlt"
+ received: "&a Du hast [number] von [name] erhalten"
+ admin:
+ eco:
+ description: "verwalte das Geld von Spielern in dieser Welt"
+ give:
+ description: "gib einem Spieler Geld"
+ parameters: " "
+ success: "&a [name] Geld gegeben. Neuer Kontostand: [number]"
+ take:
+ description: "nimm einem Spieler Geld weg"
+ parameters: " "
+ success: "&a [name] Geld weggenommen. Neuer Kontostand: [number]"
+ set:
+ description: "setze den Kontostand eines Spielers"
+ parameters: " "
+ success: "&a Kontostand von [name] auf [number] gesetzt"
+ balance:
+ description: "zeige den Kontostand eines Spielers"
+ parameters: ""
+ balance: "&a Kontostand von [name]: &e[number]"
+ errors:
+ no-economy: "&c Die Wirtschaft ist nicht verfügbar."
+ insufficient-funds: "&c Nicht genügend Geld."
+ not-a-number: "&c '[number]' ist keine gültige Zahl."
+ must-be-positive: "&c Der Betrag muss positiv sein."
+ cannot-pay-self: "&c Du kannst dir nicht selbst Geld zahlen."
diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml
new file mode 100644
index 0000000..c9ebe87
--- /dev/null
+++ b/src/main/resources/locales/en-US.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - English (US)
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "show your money balance for this world"
+ balance: "&a Balance: &e[number]"
+ pay:
+ description: "pay another player"
+ parameters: " "
+ sent: "&a You paid [number] to [name]"
+ received: "&a You received [number] from [name]"
+ admin:
+ eco:
+ description: "manage player money for this world"
+ give:
+ description: "give money to a player"
+ parameters: " "
+ success: "&a Gave money to [name]. New balance: [number]"
+ take:
+ description: "take money from a player"
+ parameters: " "
+ success: "&a Took money from [name]. New balance: [number]"
+ set:
+ description: "set a player's balance"
+ parameters: " "
+ success: "&a Set [name]'s balance to [number]"
+ balance:
+ description: "show a player's balance"
+ parameters: ""
+ balance: "&a [name]'s balance: &e[number]"
+ errors:
+ no-economy: "&c The economy is not available."
+ insufficient-funds: "&c Insufficient funds."
+ not-a-number: "&c '[number]' is not a valid number."
+ must-be-positive: "&c The amount must be positive."
+ cannot-pay-self: "&c You cannot pay yourself."
diff --git a/src/main/resources/locales/es.yml b/src/main/resources/locales/es.yml
new file mode 100644
index 0000000..76164d5
--- /dev/null
+++ b/src/main/resources/locales/es.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Spanish
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "muestra tu saldo en este mundo"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "paga a otro jugador"
+ parameters: " "
+ sent: "&a Has pagado [number] a [name]"
+ received: "&a Has recibido [number] de [name]"
+ admin:
+ eco:
+ description: "gestiona el dinero de los jugadores en este mundo"
+ give:
+ description: "da dinero a un jugador"
+ parameters: " "
+ success: "&a Dinero dado a [name]. Nuevo saldo: [number]"
+ take:
+ description: "quita dinero a un jugador"
+ parameters: " "
+ success: "&a Dinero quitado a [name]. Nuevo saldo: [number]"
+ set:
+ description: "establece el saldo de un jugador"
+ parameters: " "
+ success: "&a Saldo de [name] establecido en [number]"
+ balance:
+ description: "muestra el saldo de un jugador"
+ parameters: ""
+ balance: "&a Saldo de [name]: &e[number]"
+ errors:
+ no-economy: "&c La economía no está disponible."
+ insufficient-funds: "&c Fondos insuficientes."
+ not-a-number: "&c '[number]' no es un número válido."
+ must-be-positive: "&c La cantidad debe ser positiva."
+ cannot-pay-self: "&c No puedes pagarte a ti mismo."
diff --git a/src/main/resources/locales/fr.yml b/src/main/resources/locales/fr.yml
new file mode 100644
index 0000000..fc14fc7
--- /dev/null
+++ b/src/main/resources/locales/fr.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - French
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "affiche votre solde pour ce monde"
+ balance: "&a Solde : &e[number]"
+ pay:
+ description: "payer un autre joueur"
+ parameters: " "
+ sent: "&a Vous avez payé [number] à [name]"
+ received: "&a Vous avez reçu [number] de [name]"
+ admin:
+ eco:
+ description: "gérer l'argent des joueurs dans ce monde"
+ give:
+ description: "donner de l'argent à un joueur"
+ parameters: " "
+ success: "&a Argent donné à [name]. Nouveau solde : [number]"
+ take:
+ description: "retirer de l'argent à un joueur"
+ parameters: " "
+ success: "&a Argent retiré à [name]. Nouveau solde : [number]"
+ set:
+ description: "définir le solde d'un joueur"
+ parameters: " "
+ success: "&a Solde de [name] défini à [number]"
+ balance:
+ description: "afficher le solde d'un joueur"
+ parameters: ""
+ balance: "&a Solde de [name] : &e[number]"
+ errors:
+ no-economy: "&c L'économie n'est pas disponible."
+ insufficient-funds: "&c Fonds insuffisants."
+ not-a-number: "&c '[number]' n'est pas un nombre valide."
+ must-be-positive: "&c Le montant doit être positif."
+ cannot-pay-self: "&c Vous ne pouvez pas vous payer vous-même."
diff --git a/src/main/resources/locales/hr.yml b/src/main/resources/locales/hr.yml
new file mode 100644
index 0000000..8629299
--- /dev/null
+++ b/src/main/resources/locales/hr.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Croatian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "prikaži svoj saldo za ovaj svijet"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "plati drugom igraču"
+ parameters: " "
+ sent: "&a Platio si [number] igraču [name]"
+ received: "&a Primio si [number] od igrača [name]"
+ admin:
+ eco:
+ description: "upravljaj novcem igrača u ovom svijetu"
+ give:
+ description: "daj novac igraču"
+ parameters: " "
+ success: "&a Novac dan igraču [name]. Novi saldo: [number]"
+ take:
+ description: "oduzmi novac igraču"
+ parameters: " "
+ success: "&a Novac oduzet igraču [name]. Novi saldo: [number]"
+ set:
+ description: "postavi saldo igrača"
+ parameters: " "
+ success: "&a Saldo igrača [name] postavljen na [number]"
+ balance:
+ description: "prikaži saldo igrača"
+ parameters: ""
+ balance: "&a Saldo igrača [name]: &e[number]"
+ errors:
+ no-economy: "&c Ekonomija nije dostupna."
+ insufficient-funds: "&c Nedovoljno sredstava."
+ not-a-number: "&c '[number]' nije valjani broj."
+ must-be-positive: "&c Iznos mora biti pozitivan."
+ cannot-pay-self: "&c Ne možeš platiti samom sebi."
diff --git a/src/main/resources/locales/hu.yml b/src/main/resources/locales/hu.yml
new file mode 100644
index 0000000..743a000
--- /dev/null
+++ b/src/main/resources/locales/hu.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Hungarian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "megmutatja az egyenlegedet ezen a világon"
+ balance: "&a Egyenleg: &e[number]"
+ pay:
+ description: "fizetés egy másik játékosnak"
+ parameters: " "
+ sent: "&a Fizettél [number] összeget neki: [name]"
+ received: "&a Kaptál [number] összeget tőle: [name]"
+ admin:
+ eco:
+ description: "a játékosok pénzének kezelése ezen a világon"
+ give:
+ description: "pénz adása egy játékosnak"
+ parameters: " "
+ success: "&a Pénz adva neki: [name]. Új egyenleg: [number]"
+ take:
+ description: "pénz elvétele egy játékostól"
+ parameters: " "
+ success: "&a Pénz elvéve tőle: [name]. Új egyenleg: [number]"
+ set:
+ description: "egy játékos egyenlegének beállítása"
+ parameters: " "
+ success: "&a [name] egyenlege beállítva: [number]"
+ balance:
+ description: "egy játékos egyenlegének megjelenítése"
+ parameters: ""
+ balance: "&a [name] egyenlege: &e[number]"
+ errors:
+ no-economy: "&c A gazdaság nem érhető el."
+ insufficient-funds: "&c Nincs elég pénzed."
+ not-a-number: "&c '[number]' nem érvényes szám."
+ must-be-positive: "&c Az összegnek pozitívnak kell lennie."
+ cannot-pay-self: "&c Nem fizethetsz saját magadnak."
diff --git a/src/main/resources/locales/id.yml b/src/main/resources/locales/id.yml
new file mode 100644
index 0000000..cebeb93
--- /dev/null
+++ b/src/main/resources/locales/id.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Indonesian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "tampilkan saldo uangmu untuk dunia ini"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "bayar pemain lain"
+ parameters: " "
+ sent: "&a Kamu membayar [number] kepada [name]"
+ received: "&a Kamu menerima [number] dari [name]"
+ admin:
+ eco:
+ description: "kelola uang pemain untuk dunia ini"
+ give:
+ description: "beri uang kepada pemain"
+ parameters: " "
+ success: "&a Memberi uang kepada [name]. Saldo baru: [number]"
+ take:
+ description: "ambil uang dari pemain"
+ parameters: " "
+ success: "&a Mengambil uang dari [name]. Saldo baru: [number]"
+ set:
+ description: "atur saldo pemain"
+ parameters: " "
+ success: "&a Saldo [name] diatur menjadi [number]"
+ balance:
+ description: "tampilkan saldo pemain"
+ parameters: ""
+ balance: "&a Saldo [name]: &e[number]"
+ errors:
+ no-economy: "&c Ekonomi tidak tersedia."
+ insufficient-funds: "&c Dana tidak cukup."
+ not-a-number: "&c '[number]' bukan angka yang valid."
+ must-be-positive: "&c Jumlah harus positif."
+ cannot-pay-self: "&c Kamu tidak bisa membayar dirimu sendiri."
diff --git a/src/main/resources/locales/it.yml b/src/main/resources/locales/it.yml
new file mode 100644
index 0000000..a2bd4d4
--- /dev/null
+++ b/src/main/resources/locales/it.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Italian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "mostra il tuo saldo per questo mondo"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "paga un altro giocatore"
+ parameters: " "
+ sent: "&a Hai pagato [number] a [name]"
+ received: "&a Hai ricevuto [number] da [name]"
+ admin:
+ eco:
+ description: "gestisci il denaro dei giocatori in questo mondo"
+ give:
+ description: "dai denaro a un giocatore"
+ parameters: " "
+ success: "&a Denaro dato a [name]. Nuovo saldo: [number]"
+ take:
+ description: "togli denaro a un giocatore"
+ parameters: " "
+ success: "&a Denaro tolto a [name]. Nuovo saldo: [number]"
+ set:
+ description: "imposta il saldo di un giocatore"
+ parameters: " "
+ success: "&a Saldo di [name] impostato a [number]"
+ balance:
+ description: "mostra il saldo di un giocatore"
+ parameters: ""
+ balance: "&a Saldo di [name]: &e[number]"
+ errors:
+ no-economy: "&c L'economia non è disponibile."
+ insufficient-funds: "&c Fondi insufficienti."
+ not-a-number: "&c '[number]' non è un numero valido."
+ must-be-positive: "&c L'importo deve essere positivo."
+ cannot-pay-self: "&c Non puoi pagare te stesso."
diff --git a/src/main/resources/locales/ja.yml b/src/main/resources/locales/ja.yml
new file mode 100644
index 0000000..0aec612
--- /dev/null
+++ b/src/main/resources/locales/ja.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Japanese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "このワールドでの所持金を表示します"
+ balance: "&a 残高: &e[number]"
+ pay:
+ description: "他のプレイヤーに支払います"
+ parameters: " "
+ sent: "&a [name] に [number] を支払いました"
+ received: "&a [name] から [number] を受け取りました"
+ admin:
+ eco:
+ description: "このワールドのプレイヤーの所持金を管理します"
+ give:
+ description: "プレイヤーにお金を与えます"
+ parameters: " "
+ success: "&a [name] にお金を与えました。新しい残高: [number]"
+ take:
+ description: "プレイヤーからお金を取り上げます"
+ parameters: " "
+ success: "&a [name] からお金を取り上げました。新しい残高: [number]"
+ set:
+ description: "プレイヤーの残高を設定します"
+ parameters: " "
+ success: "&a [name] の残高を [number] に設定しました"
+ balance:
+ description: "プレイヤーの残高を表示します"
+ parameters: ""
+ balance: "&a [name] の残高: &e[number]"
+ errors:
+ no-economy: "&c 経済機能が利用できません。"
+ insufficient-funds: "&c 残高が不足しています。"
+ not-a-number: "&c '[number]' は有効な数値ではありません。"
+ must-be-positive: "&c 金額は正の値である必要があります。"
+ cannot-pay-self: "&c 自分自身に支払うことはできません。"
diff --git a/src/main/resources/locales/ko.yml b/src/main/resources/locales/ko.yml
new file mode 100644
index 0000000..51ed7a9
--- /dev/null
+++ b/src/main/resources/locales/ko.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Korean
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "이 월드에서의 잔액을 표시합니다"
+ balance: "&a 잔액: &e[number]"
+ pay:
+ description: "다른 플레이어에게 지불합니다"
+ parameters: " "
+ sent: "&a [name] 님에게 [number] 을(를) 지불했습니다"
+ received: "&a [name] 님으로부터 [number] 을(를) 받았습니다"
+ admin:
+ eco:
+ description: "이 월드의 플레이어 돈을 관리합니다"
+ give:
+ description: "플레이어에게 돈을 지급합니다"
+ parameters: " "
+ success: "&a [name] 님에게 돈을 지급했습니다. 새 잔액: [number]"
+ take:
+ description: "플레이어에게서 돈을 회수합니다"
+ parameters: " "
+ success: "&a [name] 님에게서 돈을 회수했습니다. 새 잔액: [number]"
+ set:
+ description: "플레이어의 잔액을 설정합니다"
+ parameters: " "
+ success: "&a [name] 님의 잔액을 [number] (으)로 설정했습니다"
+ balance:
+ description: "플레이어의 잔액을 표시합니다"
+ parameters: ""
+ balance: "&a [name] 님의 잔액: &e[number]"
+ errors:
+ no-economy: "&c 경제 시스템을 사용할 수 없습니다."
+ insufficient-funds: "&c 잔액이 부족합니다."
+ not-a-number: "&c '[number]' 은(는) 올바른 숫자가 아닙니다."
+ must-be-positive: "&c 금액은 0보다 커야 합니다."
+ cannot-pay-self: "&c 자기 자신에게 지불할 수 없습니다."
diff --git a/src/main/resources/locales/lv.yml b/src/main/resources/locales/lv.yml
new file mode 100644
index 0000000..d79b1f1
--- /dev/null
+++ b/src/main/resources/locales/lv.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Latvian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "parāda tavu naudas atlikumu šajā pasaulē"
+ balance: "&a Atlikums: &e[number]"
+ pay:
+ description: "samaksāt citam spēlētājam"
+ parameters: " "
+ sent: "&a Tu samaksāji [number] spēlētājam [name]"
+ received: "&a Tu saņēmi [number] no spēlētāja [name]"
+ admin:
+ eco:
+ description: "pārvaldīt spēlētāju naudu šajā pasaulē"
+ give:
+ description: "iedot spēlētājam naudu"
+ parameters: " "
+ success: "&a Nauda iedota spēlētājam [name]. Jaunais atlikums: [number]"
+ take:
+ description: "atņemt spēlētājam naudu"
+ parameters: " "
+ success: "&a Nauda atņemta spēlētājam [name]. Jaunais atlikums: [number]"
+ set:
+ description: "iestatīt spēlētāja atlikumu"
+ parameters: " "
+ success: "&a Spēlētāja [name] atlikums iestatīts uz [number]"
+ balance:
+ description: "parādīt spēlētāja atlikumu"
+ parameters: ""
+ balance: "&a Spēlētāja [name] atlikums: &e[number]"
+ errors:
+ no-economy: "&c Ekonomika nav pieejama."
+ insufficient-funds: "&c Nepietiek naudas."
+ not-a-number: "&c '[number]' nav derīgs skaitlis."
+ must-be-positive: "&c Summai jābūt pozitīvai."
+ cannot-pay-self: "&c Tu nevari samaksāt pats sev."
diff --git a/src/main/resources/locales/nl.yml b/src/main/resources/locales/nl.yml
new file mode 100644
index 0000000..c8446cd
--- /dev/null
+++ b/src/main/resources/locales/nl.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Dutch
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "toon je saldo voor deze wereld"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "betaal een andere speler"
+ parameters: " "
+ sent: "&a Je hebt [number] aan [name] betaald"
+ received: "&a Je hebt [number] van [name] ontvangen"
+ admin:
+ eco:
+ description: "beheer het geld van spelers in deze wereld"
+ give:
+ description: "geef geld aan een speler"
+ parameters: " "
+ success: "&a Geld gegeven aan [name]. Nieuw saldo: [number]"
+ take:
+ description: "neem geld af van een speler"
+ parameters: " "
+ success: "&a Geld afgenomen van [name]. Nieuw saldo: [number]"
+ set:
+ description: "stel het saldo van een speler in"
+ parameters: " "
+ success: "&a Saldo van [name] ingesteld op [number]"
+ balance:
+ description: "toon het saldo van een speler"
+ parameters: ""
+ balance: "&a Saldo van [name]: &e[number]"
+ errors:
+ no-economy: "&c De economie is niet beschikbaar."
+ insufficient-funds: "&c Onvoldoende geld."
+ not-a-number: "&c '[number]' is geen geldig getal."
+ must-be-positive: "&c Het bedrag moet positief zijn."
+ cannot-pay-self: "&c Je kunt niet aan jezelf betalen."
diff --git a/src/main/resources/locales/pl.yml b/src/main/resources/locales/pl.yml
new file mode 100644
index 0000000..2402a07
--- /dev/null
+++ b/src/main/resources/locales/pl.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Polish
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "pokaż swój stan konta dla tego świata"
+ balance: "&a Stan konta: &e[number]"
+ pay:
+ description: "zapłać innemu graczowi"
+ parameters: " "
+ sent: "&a Zapłaciłeś [number] graczowi [name]"
+ received: "&a Otrzymałeś [number] od gracza [name]"
+ admin:
+ eco:
+ description: "zarządzaj pieniędzmi graczy w tym świecie"
+ give:
+ description: "daj graczowi pieniądze"
+ parameters: " "
+ success: "&a Przekazano pieniądze graczowi [name]. Nowy stan konta: [number]"
+ take:
+ description: "zabierz graczowi pieniądze"
+ parameters: " "
+ success: "&a Zabrano pieniądze graczowi [name]. Nowy stan konta: [number]"
+ set:
+ description: "ustaw stan konta gracza"
+ parameters: " "
+ success: "&a Stan konta gracza [name] ustawiono na [number]"
+ balance:
+ description: "pokaż stan konta gracza"
+ parameters: ""
+ balance: "&a Stan konta gracza [name]: &e[number]"
+ errors:
+ no-economy: "&c Ekonomia jest niedostępna."
+ insufficient-funds: "&c Niewystarczające środki."
+ not-a-number: "&c '[number]' nie jest prawidłową liczbą."
+ must-be-positive: "&c Kwota musi być dodatnia."
+ cannot-pay-self: "&c Nie możesz zapłacić samemu sobie."
diff --git a/src/main/resources/locales/pt-BR.yml b/src/main/resources/locales/pt-BR.yml
new file mode 100644
index 0000000..3514967
--- /dev/null
+++ b/src/main/resources/locales/pt-BR.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Brazilian Portuguese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "mostra seu saldo neste mundo"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "pague a outro jogador"
+ parameters: " "
+ sent: "&a Você pagou [number] para [name]"
+ received: "&a Você recebeu [number] de [name]"
+ admin:
+ eco:
+ description: "gerencie o dinheiro dos jogadores neste mundo"
+ give:
+ description: "dê dinheiro a um jogador"
+ parameters: " "
+ success: "&a Dinheiro dado a [name]. Novo saldo: [number]"
+ take:
+ description: "retire dinheiro de um jogador"
+ parameters: " "
+ success: "&a Dinheiro retirado de [name]. Novo saldo: [number]"
+ set:
+ description: "defina o saldo de um jogador"
+ parameters: " "
+ success: "&a Saldo de [name] definido para [number]"
+ balance:
+ description: "mostre o saldo de um jogador"
+ parameters: ""
+ balance: "&a Saldo de [name]: &e[number]"
+ errors:
+ no-economy: "&c A economia não está disponível."
+ insufficient-funds: "&c Fundos insuficientes."
+ not-a-number: "&c '[number]' não é um número válido."
+ must-be-positive: "&c A quantia deve ser positiva."
+ cannot-pay-self: "&c Você não pode pagar a si mesmo."
diff --git a/src/main/resources/locales/pt.yml b/src/main/resources/locales/pt.yml
new file mode 100644
index 0000000..74dbe5c
--- /dev/null
+++ b/src/main/resources/locales/pt.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Portuguese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "mostra o teu saldo neste mundo"
+ balance: "&a Saldo: &e[number]"
+ pay:
+ description: "paga a outro jogador"
+ parameters: " "
+ sent: "&a Pagaste [number] a [name]"
+ received: "&a Recebeste [number] de [name]"
+ admin:
+ eco:
+ description: "gere o dinheiro dos jogadores neste mundo"
+ give:
+ description: "dá dinheiro a um jogador"
+ parameters: " "
+ success: "&a Dinheiro dado a [name]. Novo saldo: [number]"
+ take:
+ description: "retira dinheiro a um jogador"
+ parameters: " "
+ success: "&a Dinheiro retirado a [name]. Novo saldo: [number]"
+ set:
+ description: "define o saldo de um jogador"
+ parameters: " "
+ success: "&a Saldo de [name] definido para [number]"
+ balance:
+ description: "mostra o saldo de um jogador"
+ parameters: ""
+ balance: "&a Saldo de [name]: &e[number]"
+ errors:
+ no-economy: "&c A economia não está disponível."
+ insufficient-funds: "&c Fundos insuficientes."
+ not-a-number: "&c '[number]' não é um número válido."
+ must-be-positive: "&c O valor deve ser positivo."
+ cannot-pay-self: "&c Não podes pagar a ti próprio."
diff --git a/src/main/resources/locales/ro.yml b/src/main/resources/locales/ro.yml
new file mode 100644
index 0000000..1614ec7
--- /dev/null
+++ b/src/main/resources/locales/ro.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Romanian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "afișează soldul tău pentru această lume"
+ balance: "&a Sold: &e[number]"
+ pay:
+ description: "plătește alt jucător"
+ parameters: " "
+ sent: "&a Ai plătit [number] lui [name]"
+ received: "&a Ai primit [number] de la [name]"
+ admin:
+ eco:
+ description: "gestionează banii jucătorilor în această lume"
+ give:
+ description: "dă bani unui jucător"
+ parameters: " "
+ success: "&a Bani dați lui [name]. Sold nou: [number]"
+ take:
+ description: "ia bani de la un jucător"
+ parameters: " "
+ success: "&a Bani luați de la [name]. Sold nou: [number]"
+ set:
+ description: "setează soldul unui jucător"
+ parameters: " "
+ success: "&a Soldul lui [name] setat la [number]"
+ balance:
+ description: "afișează soldul unui jucător"
+ parameters: ""
+ balance: "&a Soldul lui [name]: &e[number]"
+ errors:
+ no-economy: "&c Economia nu este disponibilă."
+ insufficient-funds: "&c Fonduri insuficiente."
+ not-a-number: "&c '[number]' nu este un număr valid."
+ must-be-positive: "&c Suma trebuie să fie pozitivă."
+ cannot-pay-self: "&c Nu te poți plăti pe tine însuți."
diff --git a/src/main/resources/locales/ru.yml b/src/main/resources/locales/ru.yml
new file mode 100644
index 0000000..77ccade
--- /dev/null
+++ b/src/main/resources/locales/ru.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Russian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "показать ваш баланс для этого мира"
+ balance: "&a Баланс: &e[number]"
+ pay:
+ description: "заплатить другому игроку"
+ parameters: " "
+ sent: "&a Вы заплатили [number] игроку [name]"
+ received: "&a Вы получили [number] от игрока [name]"
+ admin:
+ eco:
+ description: "управлять деньгами игроков в этом мире"
+ give:
+ description: "выдать деньги игроку"
+ parameters: " "
+ success: "&a Деньги выданы игроку [name]. Новый баланс: [number]"
+ take:
+ description: "забрать деньги у игрока"
+ parameters: " "
+ success: "&a Деньги забраны у игрока [name]. Новый баланс: [number]"
+ set:
+ description: "установить баланс игрока"
+ parameters: " "
+ success: "&a Баланс игрока [name] установлен на [number]"
+ balance:
+ description: "показать баланс игрока"
+ parameters: ""
+ balance: "&a Баланс игрока [name]: &e[number]"
+ errors:
+ no-economy: "&c Экономика недоступна."
+ insufficient-funds: "&c Недостаточно средств."
+ not-a-number: "&c '[number]' не является допустимым числом."
+ must-be-positive: "&c Сумма должна быть положительной."
+ cannot-pay-self: "&c Вы не можете заплатить самому себе."
diff --git a/src/main/resources/locales/tr.yml b/src/main/resources/locales/tr.yml
new file mode 100644
index 0000000..28c3acb
--- /dev/null
+++ b/src/main/resources/locales/tr.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Turkish
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "bu dünyadaki bakiyeni gösterir"
+ balance: "&a Bakiye: &e[number]"
+ pay:
+ description: "başka bir oyuncuya öde"
+ parameters: " "
+ sent: "&a [name] oyuncusuna [number] ödedin"
+ received: "&a [name] oyuncusundan [number] aldın"
+ admin:
+ eco:
+ description: "bu dünyadaki oyuncuların parasını yönet"
+ give:
+ description: "bir oyuncuya para ver"
+ parameters: " "
+ success: "&a [name] oyuncusuna para verildi. Yeni bakiye: [number]"
+ take:
+ description: "bir oyuncudan para al"
+ parameters: " "
+ success: "&a [name] oyuncusundan para alındı. Yeni bakiye: [number]"
+ set:
+ description: "bir oyuncunun bakiyesini ayarla"
+ parameters: " "
+ success: "&a [name] oyuncusunun bakiyesi [number] olarak ayarlandı"
+ balance:
+ description: "bir oyuncunun bakiyesini göster"
+ parameters: ""
+ balance: "&a [name] oyuncusunun bakiyesi: &e[number]"
+ errors:
+ no-economy: "&c Ekonomi kullanılamıyor."
+ insufficient-funds: "&c Yetersiz bakiye."
+ not-a-number: "&c '[number]' geçerli bir sayı değil."
+ must-be-positive: "&c Miktar pozitif olmalıdır."
+ cannot-pay-self: "&c Kendine ödeme yapamazsın."
diff --git a/src/main/resources/locales/uk.yml b/src/main/resources/locales/uk.yml
new file mode 100644
index 0000000..0e8026f
--- /dev/null
+++ b/src/main/resources/locales/uk.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Ukrainian
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "показати ваш баланс для цього світу"
+ balance: "&a Баланс: &e[number]"
+ pay:
+ description: "заплатити іншому гравцеві"
+ parameters: " "
+ sent: "&a Ви заплатили [number] гравцеві [name]"
+ received: "&a Ви отримали [number] від гравця [name]"
+ admin:
+ eco:
+ description: "керувати грошима гравців у цьому світі"
+ give:
+ description: "видати гроші гравцеві"
+ parameters: " "
+ success: "&a Гроші видано гравцеві [name]. Новий баланс: [number]"
+ take:
+ description: "забрати гроші у гравця"
+ parameters: " "
+ success: "&a Гроші забрано у гравця [name]. Новий баланс: [number]"
+ set:
+ description: "встановити баланс гравця"
+ parameters: " "
+ success: "&a Баланс гравця [name] встановлено на [number]"
+ balance:
+ description: "показати баланс гравця"
+ parameters: ""
+ balance: "&a Баланс гравця [name]: &e[number]"
+ errors:
+ no-economy: "&c Економіка недоступна."
+ insufficient-funds: "&c Недостатньо коштів."
+ not-a-number: "&c '[number]' не є дійсним числом."
+ must-be-positive: "&c Сума повинна бути додатною."
+ cannot-pay-self: "&c Ви не можете заплатити самому собі."
diff --git a/src/main/resources/locales/vi.yml b/src/main/resources/locales/vi.yml
new file mode 100644
index 0000000..f13020e
--- /dev/null
+++ b/src/main/resources/locales/vi.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Vietnamese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "hiển thị số dư của bạn cho thế giới này"
+ balance: "&a Số dư: &e[number]"
+ pay:
+ description: "trả tiền cho người chơi khác"
+ parameters: " "
+ sent: "&a Bạn đã trả [number] cho [name]"
+ received: "&a Bạn đã nhận [number] từ [name]"
+ admin:
+ eco:
+ description: "quản lý tiền của người chơi trong thế giới này"
+ give:
+ description: "đưa tiền cho người chơi"
+ parameters: " "
+ success: "&a Đã đưa tiền cho [name]. Số dư mới: [number]"
+ take:
+ description: "lấy tiền từ người chơi"
+ parameters: " "
+ success: "&a Đã lấy tiền từ [name]. Số dư mới: [number]"
+ set:
+ description: "đặt số dư của người chơi"
+ parameters: " "
+ success: "&a Đã đặt số dư của [name] thành [number]"
+ balance:
+ description: "hiển thị số dư của người chơi"
+ parameters: ""
+ balance: "&a Số dư của [name]: &e[number]"
+ errors:
+ no-economy: "&c Nền kinh tế không khả dụng."
+ insufficient-funds: "&c Không đủ tiền."
+ not-a-number: "&c '[number]' không phải là số hợp lệ."
+ must-be-positive: "&c Số tiền phải là số dương."
+ cannot-pay-self: "&c Bạn không thể trả tiền cho chính mình."
diff --git a/src/main/resources/locales/zh-CN.yml b/src/main/resources/locales/zh-CN.yml
new file mode 100644
index 0000000..620f6f4
--- /dev/null
+++ b/src/main/resources/locales/zh-CN.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Simplified Chinese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "显示你在此世界的余额"
+ balance: "&a 余额:&e[number]"
+ pay:
+ description: "向其他玩家付款"
+ parameters: " "
+ sent: "&a 你已向 [name] 支付 [number]"
+ received: "&a 你已从 [name] 收到 [number]"
+ admin:
+ eco:
+ description: "管理此世界中玩家的金钱"
+ give:
+ description: "给予玩家金钱"
+ parameters: " "
+ success: "&a 已给予 [name] 金钱。新余额:[number]"
+ take:
+ description: "扣除玩家金钱"
+ parameters: " "
+ success: "&a 已扣除 [name] 金钱。新余额:[number]"
+ set:
+ description: "设置玩家的余额"
+ parameters: " "
+ success: "&a 已将 [name] 的余额设置为 [number]"
+ balance:
+ description: "显示玩家的余额"
+ parameters: ""
+ balance: "&a [name] 的余额:&e[number]"
+ errors:
+ no-economy: "&c 经济功能不可用。"
+ insufficient-funds: "&c 余额不足。"
+ not-a-number: "&c '[number]' 不是有效的数字。"
+ must-be-positive: "&c 金额必须为正数。"
+ cannot-pay-self: "&c 你不能向自己付款。"
diff --git a/src/main/resources/locales/zh-HK.yml b/src/main/resources/locales/zh-HK.yml
new file mode 100644
index 0000000..c74cc74
--- /dev/null
+++ b/src/main/resources/locales/zh-HK.yml
@@ -0,0 +1,38 @@
+###########################################################################################
+# InvSwitcher locale file - Traditional Chinese
+###########################################################################################
+invswitcher:
+ commands:
+ balance:
+ description: "顯示你在此世界的餘額"
+ balance: "&a 餘額:&e[number]"
+ pay:
+ description: "向其他玩家付款"
+ parameters: " "
+ sent: "&a 你已向 [name] 支付 [number]"
+ received: "&a 你已從 [name] 收到 [number]"
+ admin:
+ eco:
+ description: "管理此世界中玩家的金錢"
+ give:
+ description: "給予玩家金錢"
+ parameters: " "
+ success: "&a 已給予 [name] 金錢。新餘額:[number]"
+ take:
+ description: "扣除玩家金錢"
+ parameters: " "
+ success: "&a 已扣除 [name] 金錢。新餘額:[number]"
+ set:
+ description: "設定玩家的餘額"
+ parameters: " "
+ success: "&a 已將 [name] 的餘額設定為 [number]"
+ balance:
+ description: "顯示玩家的餘額"
+ parameters: ""
+ balance: "&a [name] 的餘額:&e[number]"
+ errors:
+ no-economy: "&c 經濟功能無法使用。"
+ insufficient-funds: "&c 餘額不足。"
+ not-a-number: "&c '[number]' 不是有效的數字。"
+ must-be-positive: "&c 金額必須為正數。"
+ cannot-pay-self: "&c 你不能向自己付款。"
diff --git a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java
index dfe5419..03c61b2 100644
--- a/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java
+++ b/src/test/java/com/wasteofplastic/invswitcher/StoreTest.java
@@ -22,6 +22,7 @@
import java.nio.file.Path;
import java.util.Comparator;
import java.util.HashSet;
+import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -30,8 +31,11 @@
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.World.Environment;
+import org.bukkit.advancement.Advancement;
+import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@@ -200,6 +204,46 @@ public void testGetInventory() {
verify(player).setTotalExperience(0);
}
+ /**
+ * Test that advancement grants during {@link Store#getInventory} do not modify the player's
+ * experience points. Some advancements reward XP when their criteria are awarded; the store
+ * must save and restore XP around the advancement grant step.
+ */
+ @Test
+ public void testGetInventoryAdvancementsPreservesExperience() {
+ sets.setAdvancements(true);
+ sets.setExperience(true);
+ sets.setStatistics(false);
+ sets.setIslandsActive(false);
+
+ // Mock an advancement with awarded criteria
+ Advancement advancement = mock(Advancement.class);
+ NamespacedKey advKey = NamespacedKey.minecraft("story_mine_stone");
+ when(advancement.getKey()).thenReturn(advKey);
+
+ AdvancementProgress progress = mock(AdvancementProgress.class);
+ Set criteria = new HashSet<>(Set.of("mine_stone"));
+ when(progress.getAwardedCriteria()).thenReturn(criteria);
+ when(player.getAdvancementProgress(advancement)).thenReturn(progress);
+
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ // Return a fresh iterator each time so both storeInventory and getInventory can iterate
+ mockedBukkit.when(Bukkit::advancementIterator).thenAnswer(inv -> List.of(advancement).iterator());
+
+ // Store inventory (saves advancement data and clears player including XP reset)
+ s.storeInventory(player, world);
+
+ // Load inventory — experience set, advancements granted, XP restored
+ s.getInventory(player, world);
+ }
+
+ // setTotalExperience should be called exactly 3 times:
+ // 1. clearPlayer during storeInventory (XP reset to 0)
+ // 2. experience loading during getInventory (XP set to stored value 0)
+ // 3. XP restoration inside setAdvancements after granting advancement criteria
+ verify(player, times(3)).setTotalExperience(0);
+ }
+
/**
* Test method for {@link com.wasteofplastic.invswitcher.Store#removeFromCache(org.bukkit.entity.Player)}.
*/
@@ -601,4 +645,217 @@ public void testMigrationFromOldWorldKey() {
verify(player.getInventory(), atLeastOnce()).setContents(any(ItemStack[].class));
}
+ // --- clearStoredXForWorld Tests ---
+
+ /**
+ * When inventory is enabled and the event fires for a world the player is not in,
+ * the stored inventory for that world should be cleared.
+ * After clearing, loading inventory for that world should give empty contents.
+ */
+ @Test
+ public void testClearStoredInventoryForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ // First save something in the store
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ assertTrue(s.isWorldStored(player, world), "Inventory should be stored before clearing");
+
+ // Clear stored inventory for the world
+ s.clearStoredInventoryForWorld(player, world, island);
+
+ // isWorldStored returns true even after clearing (entry exists but is empty list)
+ assertTrue(s.isWorldStored(player, world));
+
+ // Load inventory back - should set empty contents to player
+ s.getInventory(player, world);
+ // setContents should have been called with empty array
+ verify(player.getInventory(), atLeastOnce()).setContents(any(ItemStack[].class));
+ }
+ }
+
+ /**
+ * When ender chest is enabled, clearStoredEnderChestForWorld should work without error.
+ */
+ @Test
+ public void testClearStoredEnderChestForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // Should not throw
+ s.clearStoredEnderChestForWorld(player, world, island);
+ }
+ }
+
+ /**
+ * When experience is enabled, clearStoredExpForWorld should zero out the stored exp.
+ */
+ @Test
+ public void testClearStoredExpForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ when(player.getTotalExperience()).thenReturn(500);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // Should not throw
+ s.clearStoredExpForWorld(player, world, island);
+ }
+ }
+
+ /**
+ * When money is enabled, clearStoredMoneyForWorld should zero the stored balance for the world.
+ */
+ @Test
+ public void testClearStoredMoneyForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ sets.setMoney(true);
+ Island island = mock(Island.class);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // Seed a balance for the world
+ s.getStorageObject(playerUUID).setMoney("world", 500D);
+ // Clear it
+ s.clearStoredMoneyForWorld(player, world, island);
+ }
+ assertEquals(0D, s.getStorageObject(playerUUID).getMoney("world"), 0.0001);
+ }
+
+ /**
+ * clearStoredMoneyForWorld should be a no-op when money is disabled in settings.
+ */
+ @Test
+ public void testClearStoredMoneyForWorldMoneyDisabled() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ sets.setMoney(false);
+ Island island = mock(Island.class);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ s.getStorageObject(playerUUID).setMoney("world", 500D);
+ s.clearStoredMoneyForWorld(player, world, island);
+ }
+ // Balance untouched
+ assertEquals(500D, s.getStorageObject(playerUUID).getMoney("world"), 0.0001);
+ }
+
+ /**
+ * When health is enabled, clearStoredHealthForWorld should work without error.
+ */
+ @Test
+ public void testClearStoredHealthForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ when(player.getHealth()).thenReturn(10.0);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // Should not throw
+ s.clearStoredHealthForWorld(player, world, island);
+ }
+ }
+
+ /**
+ * When food is enabled, clearStoredFoodForWorld should work without error.
+ */
+ @Test
+ public void testClearStoredFoodForWorld() {
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ when(player.getFoodLevel()).thenReturn(8);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // Should not throw
+ s.clearStoredFoodForWorld(player, world, island);
+ }
+ }
+
+ /**
+ * clearStoredInventoryForWorld should be a no-op when inventory is disabled in settings.
+ * No exceptions should be thrown.
+ */
+ @Test
+ public void testClearStoredInventoryForWorldInventoryDisabled() {
+ sets.setInventory(false);
+ sets.setStatistics(false);
+ sets.setAdvancements(false);
+ Island island = mock(Island.class);
+ try (MockedStatic mockedBukkit = mockStatic(Bukkit.class, Mockito.RETURNS_MOCKS)) {
+ s.storeInventory(player, world);
+ // With inventory disabled, nothing is stored in the inventory map
+ assertFalse(s.isWorldStored(player, world));
+
+ // Calling clear should be a no-op (no exception, no effect)
+ s.clearStoredInventoryForWorld(player, world, island);
+ assertFalse(s.isWorldStored(player, world));
+ }
+ }
+
+ /**
+ * getStorageKeyForEvent should return the world name when islands mode is inactive.
+ */
+ @Test
+ public void testGetStorageKeyForEventIslandsDisabled() {
+ sets.setIslandsActive(false);
+ Island island = mock(Island.class);
+ String key = s.getStorageKeyForEvent(player, world, island);
+ assertEquals("world", key);
+ }
+
+ /**
+ * getStorageKeyForEvent should return world name when player has only 1 island.
+ */
+ @Test
+ public void testGetStorageKeyForEventSingleIsland() {
+ sets.setIslandsActive(true);
+ Island island = mock(Island.class);
+ try (MockedStatic mockedUtil = mockStatic(Util.class)) {
+ mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world);
+ when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(1);
+ String key = s.getStorageKeyForEvent(player, world, island);
+ assertEquals("world", key);
+ }
+ }
+
+ /**
+ * getStorageKeyForEvent should return island-specific key when player owns the island
+ * and has multiple islands.
+ */
+ @Test
+ public void testGetStorageKeyForEventMultipleIslandsOwner() {
+ sets.setIslandsActive(true);
+ Island island = mock(Island.class);
+ when(island.getOwner()).thenReturn(playerUUID);
+ when(island.getUniqueId()).thenReturn("island-abc");
+ try (MockedStatic mockedUtil = mockStatic(Util.class)) {
+ mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world);
+ when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2);
+ String key = s.getStorageKeyForEvent(player, world, island);
+ assertEquals("world/island-abc", key);
+ }
+ }
+
+ /**
+ * getStorageKeyForEvent should return just the world name when player does not own the island
+ * (e.g., kicked from a team). Uses the world-level key since the player is a member, not owner.
+ */
+ @Test
+ public void testGetStorageKeyForEventMultipleIslandsNotOwner() {
+ sets.setIslandsActive(true);
+ Island island = mock(Island.class);
+ UUID otherOwner = UUID.randomUUID();
+ when(island.getOwner()).thenReturn(otherOwner); // player is NOT the owner
+ try (MockedStatic mockedUtil = mockStatic(Util.class)) {
+ mockedUtil.when(() -> Util.getWorld(world)).thenReturn(world);
+ when(islandsManager.getNumberOfConcurrentIslands(playerUUID, world)).thenReturn(2);
+ String key = s.getStorageKeyForEvent(player, world, island);
+ assertEquals("world", key); // Falls back to world name
+ }
+ }
+
}
diff --git a/src/test/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorageTest.java b/src/test/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorageTest.java
index a450181..439ef82 100644
--- a/src/test/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorageTest.java
+++ b/src/test/java/com/wasteofplastic/invswitcher/dataobjects/InventoryStorageTest.java
@@ -381,4 +381,25 @@ public void testGetEntityStats() {
}
+ /**
+ * The money map can be null after Gson deserialization of pre-1.18.0 records (field
+ * initializers are bypassed). setMoney must lazily create the map rather than throw.
+ */
+ @Test
+ public void testSetMoneyWhenMapNull() {
+ is.setMoney((Map) null);
+ is.setMoney("world", 100.0);
+ assertEquals(100.0, is.getMoney("world"), 0.0001);
+ }
+
+ /**
+ * clearWorldData must not throw when the money map is null.
+ */
+ @Test
+ public void testClearWorldDataWhenMoneyNull() {
+ is.setMoney((Map) null);
+ is.clearWorldData("world");
+ assertNull(is.getMoney("world"));
+ }
+
}
diff --git a/src/test/java/com/wasteofplastic/invswitcher/economy/InvEconomyTest.java b/src/test/java/com/wasteofplastic/invswitcher/economy/InvEconomyTest.java
new file mode 100644
index 0000000..9b42e2c
--- /dev/null
+++ b/src/test/java/com/wasteofplastic/invswitcher/economy/InvEconomyTest.java
@@ -0,0 +1,290 @@
+package com.wasteofplastic.invswitcher.economy;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_MOCKS;
+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.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.mockbukkit.mockbukkit.MockBukkit;
+
+import com.wasteofplastic.invswitcher.InvSwitcher;
+import com.wasteofplastic.invswitcher.Settings;
+import com.wasteofplastic.invswitcher.Store;
+import com.wasteofplastic.invswitcher.dataobjects.InventoryStorage;
+
+import net.milkbowl.vault.economy.Economy;
+import net.milkbowl.vault.economy.EconomyResponse;
+import net.milkbowl.vault.economy.EconomyResponse.ResponseType;
+import world.bentobox.bentobox.BentoBox;
+import world.bentobox.bentobox.database.DatabaseSetup.DatabaseType;
+import world.bentobox.bentobox.managers.IslandsManager;
+
+/**
+ * Tests for {@link InvEconomy} per-world routing, delegation, starting balance and import.
+ * @author tastybento
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class InvEconomyTest {
+
+ @Mock
+ private InvSwitcher addon;
+ @Mock
+ private Player player;
+ @Mock
+ private World world;
+ @Mock
+ private world.bentobox.bentobox.Settings bbSettings;
+ @Mock
+ private IslandsManager islandsManager;
+ @Mock
+ private Logger logger;
+ @Mock
+ private Economy delegate;
+
+ private Store store;
+ private Settings sets;
+ private Set