diff --git a/pom.xml b/pom.xml index 6978d46..1c49878 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ maven-compiler-plugin - 3.13.0 + 3.14.0 21 @@ -56,6 +56,10 @@ net.kyori com.drtshock.playervaults.lib.net.kyori + + com.tcoded.folialib + com.drtshock.playervaults.lib.folialib + org.kitteh com.drtshock.playervaults.lib.org.kitteh @@ -95,6 +99,10 @@ spigot-repo https://hub.spigotmc.org/nexus/content/groups/public/ + + tcoded-releases + https://repo.tcoded.com/releases + placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ @@ -193,6 +201,12 @@ 1.21.10-R0.1-SNAPSHOT provided + + com.tcoded + FoliaLib + 0.5.1 + compile + com.googlecode.json-simple json-simple diff --git a/src/main/java/com/drtshock/playervaults/Metrics.java b/src/main/java/com/drtshock/playervaults/Metrics.java index caf9f92..7ef5b32 100644 --- a/src/main/java/com/drtshock/playervaults/Metrics.java +++ b/src/main/java/com/drtshock/playervaults/Metrics.java @@ -190,7 +190,7 @@ public void run() { } // Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) - Bukkit.getScheduler().runTask(plugin, () -> submitData()); + PlayerVaults.scheduler().runNextTick(task -> submitData()); } }, 1000 * 60 * 5, 1000 * 60 * 30); // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start diff --git a/src/main/java/com/drtshock/playervaults/PlayerVaults.java b/src/main/java/com/drtshock/playervaults/PlayerVaults.java index cace052..03c48a6 100644 --- a/src/main/java/com/drtshock/playervaults/PlayerVaults.java +++ b/src/main/java/com/drtshock/playervaults/PlayerVaults.java @@ -38,8 +38,11 @@ import com.drtshock.playervaults.vaultmanagement.EconomyOperations; import com.drtshock.playervaults.vaultmanagement.VaultManager; import com.drtshock.playervaults.vaultmanagement.VaultViewInfo; +import com.google.common.collect.Sets; import com.google.gson.Gson; -import net.kyori.adventure.audience.Audience; +import com.tcoded.folialib.FoliaLib; +import com.tcoded.folialib.impl.PlatformScheduler; +import dev.kitteh.cardboardbox.CardboardBox; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -59,8 +62,6 @@ import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; -import dev.kitteh.cardboardbox.CardboardBox; import sun.misc.Unsafe; import java.io.BufferedReader; @@ -87,6 +88,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Supplier; import java.util.logging.Level; @@ -96,13 +98,15 @@ public class PlayerVaults extends JavaPlugin { public static boolean DEBUG; private static PlayerVaults instance; - private final HashMap setSign = new HashMap<>(); + private static FoliaLib foliaLib; + private static PlatformScheduler scheduler; + private final ConcurrentHashMap setSign = new ConcurrentHashMap<>(); // Player name - VaultViewInfo - private final HashMap inVault = new HashMap<>(); + private final ConcurrentHashMap inVault = new ConcurrentHashMap<>(); // VaultViewInfo - Inventory - private final HashMap openInventories = new HashMap<>(); - private final Set blockedMats = new HashSet<>(); - private final Set blockedEnchs = new HashSet<>(); + private final ConcurrentHashMap openInventories = new ConcurrentHashMap<>(); + private final Set blockedMats = Sets.newConcurrentHashSet(); + private final Set blockedEnchs = Sets.newConcurrentHashSet(); private boolean blockWithModelData = false; private boolean blockWithoutModelData = false; private boolean useVault; @@ -126,6 +130,14 @@ public static PlayerVaults getInstance() { return instance; } + public static FoliaLib foliaLib() { + return foliaLib; + } + + public static PlatformScheduler scheduler() { + return scheduler; + } + public static void debug(String s, long start) { if (DEBUG) { instance.getLogger().log(Level.INFO, "{0} took {1}ms", new Object[]{s, (System.currentTimeMillis() - start)}); @@ -146,6 +158,8 @@ public void onEnable() { return; } instance = this; + foliaLib = new FoliaLib(this); + scheduler = foliaLib.getScheduler(); long start = System.currentTimeMillis(); long time = System.currentTimeMillis(); UpdateCheck update = new UpdateCheck("PlayerVaultsX", this.getDescription().getVersion(), this.getServer().getName(), this.getServer().getVersion()); @@ -186,17 +200,15 @@ public void onEnable() { debug("setup economy", time); if (getConf().getPurge().isEnabled()) { - getServer().getScheduler().runTaskAsynchronously(this, new Cleanup(getConf().getPurge().getDaysSinceLastEdit())); + final int days = getConf().getPurge().getDaysSinceLastEdit(); + PlayerVaults.scheduler().runLaterAsync(new Cleanup(days), 1L); } - new BukkitRunnable() { - @Override - public void run() { - if (saveQueued) { - saveSignsFile(); - } + PlayerVaults.scheduler().runTimer(() -> { + if (saveQueued) { + saveSignsFile(); } - }.runTaskTimer(this, 20, 20); + }, 20, 20); this.metrics = new Metrics(this, 6905); Plugin vault = getServer().getPluginManager().getPlugin("Vault"); @@ -296,42 +308,40 @@ public void run() { this.updateCheck = new Gson().toJson(update); if (!HelpMeCommand.likesCats) return; - new BukkitRunnable() { - @Override - public void run() { - try { - URL url = new URL("https://update.plugin.party/check"); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("POST"); - con.setDoOutput(true); - con.setRequestProperty("Content-Type", "application/json"); - con.setRequestProperty("Accept", "application/json"); - try (OutputStream out = con.getOutputStream()) { - out.write(PlayerVaults.this.updateCheck.getBytes(StandardCharsets.UTF_8)); - } - String reply = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); - Response response = new Gson().fromJson(reply, Response.class); - if (response.isSuccess()) { - if (response.isUpdateAvailable()) { - PlayerVaults.this.updateResponse = response; - if (response.isUrgent()) { - PlayerVaults.this.getServer().getOnlinePlayers().forEach(PlayerVaults.this::updateNotification); - } - PlayerVaults.this.getLogger().warning("Update available: " + response.getLatestVersion() + (response.getMessage() == null ? "" : (" - " + response.getMessage()))); + PlayerVaults.scheduler().runTimerAsync(task -> { + try { + URL url = new URL("https://update.plugin.party/check"); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod("POST"); + con.setDoOutput(true); + con.setRequestProperty("Content-Type", "application/json"); + con.setRequestProperty("Accept", "application/json"); + try (OutputStream out = con.getOutputStream()) { + out.write(PlayerVaults.this.updateCheck.getBytes(StandardCharsets.UTF_8)); + } + String reply = new BufferedReader(new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n")); + Response response = new Gson().fromJson(reply, Response.class); + if (response.isSuccess()) { + if (response.isUpdateAvailable()) { + PlayerVaults.this.updateResponse = response; + if (response.isUrgent()) { + PlayerVaults.this.getServer().getOnlinePlayers().forEach(PlayerVaults.this::updateNotification); } + PlayerVaults.this.getLogger().warning("Update available: " + response.getLatestVersion() + (response.getMessage() == null ? "" : (" - " + response.getMessage()))); + } + } else { + if (response.getMessage().equals("INVALID")) { + task.cancel(); + } else if (response.getMessage().equals("TOO_FAST")) { + // Nothing for now } else { - if (response.getMessage().equals("INVALID")) { - this.cancel(); - } else if (response.getMessage().equals("TOO_FAST")) { - // Nothing for now - } else { - PlayerVaults.this.getLogger().warning("Failed to check for updates: " + response.getMessage()); - } + PlayerVaults.this.getLogger().warning("Failed to check for updates: " + response.getMessage()); } - } catch (Exception ignored) { } + } catch (Exception ignored) { + // Ignored case/handler. } - }.runTaskTimerAsynchronously(this, 1, 20 /* ticks */ * 60 /* seconds in a minute */ * 60 /* minutes in an hour*/); + }, 1, 20 /* ticks */ * 60 /* seconds in a minute */ * 60 /* minutes in an hour*/); } private void metricsLine(String name, Callable callable) { @@ -365,15 +375,15 @@ public void onDisable() { Inventory inventory = player.getOpenInventory().getTopInventory(); if (inventory.getViewers().size() == 1) { VaultViewInfo info = this.inVault.get(player.getUniqueId().toString()); - VaultManager.getInstance().saveVault(inventory, player.getUniqueId().toString(), info.getNumber()); + VaultManager.getInstance().saveVault(inventory, info.getVaultName(), info.getNumber()); this.openInventories.remove(info.toString()); // try this to make sure that they can't make further edits if the process hangs. - player.closeInventory(); + PlayerVaults.scheduler().runAtEntity(player, task -> player.closeInventory()); } this.inVault.remove(player.getUniqueId().toString()); debug("Closing vault for " + player.getName()); - player.closeInventory(); + PlayerVaults.scheduler().runAtEntity(player, task -> player.closeInventory()); } } @@ -542,15 +552,15 @@ private void saveSignsFile() { } } - public HashMap getSetSign() { + public ConcurrentHashMap getSetSign() { return this.setSign; } - public HashMap getInVault() { + public ConcurrentHashMap getInVault() { return this.inVault; } - public HashMap getOpenInventories() { + public ConcurrentHashMap getOpenInventories() { return this.openInventories; } @@ -731,7 +741,7 @@ public Component getComponent() { } } - private final Set told = new HashSet<>(); + private final Set told = Sets.newConcurrentHashSet(); public void updateNotification(Player player) { if (updateResponse == null || !player.hasPermission(Permission.ADMIN)) { @@ -759,7 +769,7 @@ public T addException(T t) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); t.printStackTrace(printWriter); - builder.append(stringWriter.toString()); + builder.append(stringWriter); this.exceptions.add(builder.toString()); } return t; diff --git a/src/main/java/com/drtshock/playervaults/commands/ConvertCommand.java b/src/main/java/com/drtshock/playervaults/commands/ConvertCommand.java index b6c88f9..207b2a8 100644 --- a/src/main/java/com/drtshock/playervaults/commands/ConvertCommand.java +++ b/src/main/java/com/drtshock/playervaults/commands/ConvertCommand.java @@ -72,7 +72,7 @@ public boolean onCommand(final CommandSender sender, Command command, String lab } else { // Fork into background this.plugin.getTL().convertBackground().title().send(sender); - PlayerVaults.getInstance().getServer().getScheduler().runTaskLaterAsynchronously(PlayerVaults.getInstance(), () -> { + PlayerVaults.scheduler().runLaterAsync(() -> { int converted = 0; VaultOperations.setLocked(true); for (Converter converter : applicableConverters) { @@ -88,4 +88,4 @@ public boolean onCommand(final CommandSender sender, Command command, String lab } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/com/drtshock/playervaults/commands/HelpMeCommand.java b/src/main/java/com/drtshock/playervaults/commands/HelpMeCommand.java index 9e0f2fc..fce829f 100644 --- a/src/main/java/com/drtshock/playervaults/commands/HelpMeCommand.java +++ b/src/main/java/com/drtshock/playervaults/commands/HelpMeCommand.java @@ -30,7 +30,6 @@ import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitRunnable; import org.kitteh.pastegg.PasteBuilder; import org.kitteh.pastegg.PasteContent; import org.kitteh.pastegg.PasteFile; @@ -43,6 +42,8 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.Arrays; +import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.logging.Level; public class HelpMeCommand implements CommandExecutor { @@ -77,61 +78,52 @@ public boolean onCommand(final CommandSender sender, Command command, String lab mainInfo.append(" ").append(plugin.getDescription().getAuthors()).append('\n'); } - new BukkitRunnable() { - private final PasteBuilder builder = new PasteBuilder().name("PlayerVaultsX Debug") - .visibility(Visibility.UNLISTED) - .expires(ZonedDateTime.now(ZoneOffset.UTC).plusDays(3)); - private int i = 0; + PlayerVaults.scheduler().runAsync(task -> { + try { + final PasteBuilder builder = new PasteBuilder().name("PlayerVaultsX Debug") + .visibility(Visibility.UNLISTED) + .expires(ZonedDateTime.now(ZoneOffset.UTC).plusDays(3)); + final int[] i = new int[]{0}; - private void add(String name, String content) { - builder.addFile(new PasteFile(i++ + name, new PasteContent(PasteContent.ContentType.TEXT, content))); - } + final BiConsumer add = (name, content) -> + builder.addFile(new PasteFile(i[0]++ + name, new PasteContent(PasteContent.ContentType.TEXT, content))); + final Function getFile = (file) -> { + try { + return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); + } catch (IOException e) { + return e.getMessage(); + } + }; - private String getFile(Path file) { - try { - return new String(Files.readAllBytes(file), StandardCharsets.UTF_8); - } catch (IOException e) { - return e.getMessage(); + Path dataPath = plugin.getDataFolder().toPath(); + add.accept("info.txt", mainInfo.toString()); + String exceptionLog = plugin.getExceptions(); + if (exceptionLog != null) { + add.accept("exceptions.txt", exceptionLog); } - } + add.accept("config.conf", getFile.apply(dataPath.resolve("config.conf"))); - @Override - public void run() { - try { - Path dataPath = plugin.getDataFolder().toPath(); - add("info.txt", mainInfo.toString()); - String exceptionLog = plugin.getExceptions(); - if (exceptionLog != null) { - add("exceptions.txt", exceptionLog); + PasteBuilder.PasteResult result = builder.build(); + + PlayerVaults.scheduler().runNextTick(t -> { + if (result.getPaste().isPresent()) { + String delKey = result.getPaste().get().getDeletionKey().orElse("No deletion key"); + String url = "https://paste.gg/anonymous/" + result.getPaste().get().getId(); + ComponentDispatcher.send(sender, Component.text("URL generated: ").append(Component.text().clickEvent(ClickEvent.openUrl(url)).content(url))); + ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize((sender instanceof Player ? "" : "") + "Deletion key: " + delKey)); + } else { + ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize("Failed to generate output. See console for details.")); + PlayerVaults.getInstance().getLogger().warning("Received: " + result.getMessage()); } - add("config.conf", getFile(dataPath.resolve("config.conf"))); - PasteBuilder.PasteResult result = builder.build(); - new BukkitRunnable() { - @Override - public void run() { - if (result.getPaste().isPresent()) { - String delKey = result.getPaste().get().getDeletionKey().orElse("No deletion key"); - String url = "https://paste.gg/anonymous/" + result.getPaste().get().getId(); - ComponentDispatcher.send(sender, Component.text("URL generated: ").append(Component.text().clickEvent(ClickEvent.openUrl(url)).content(url))); - ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize((sender instanceof Player ? "" : "") + "Deletion key: " + delKey)); - } else { - ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize("Failed to generate output. See console for details.")); - PlayerVaults.getInstance().getLogger().warning("Received: " + result.getMessage()); - } - } - }.runTask(PlayerVaults.getInstance()); - } catch (Exception e) { - PlayerVaults.getInstance().getLogger().log(Level.SEVERE, "Failed to execute debug command", e); - new BukkitRunnable() { - @Override - public void run() { - ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize("Failed to generate output. See console for details.")); - } - }.runTask(PlayerVaults.getInstance()); - } + }); + } catch (Exception e) { + PlayerVaults.getInstance().getLogger().log(Level.SEVERE, "Failed to execute debug command", e); + PlayerVaults.scheduler().runNextTick(t -> + ComponentDispatcher.send(sender, MiniMessage.miniMessage().deserialize("Failed to generate output. See console for details.")) + ); } - }.runTaskAsynchronously(PlayerVaults.getInstance()); + }); } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/com/drtshock/playervaults/listeners/Listeners.java b/src/main/java/com/drtshock/playervaults/listeners/Listeners.java index 5e60897..1165a3b 100644 --- a/src/main/java/com/drtshock/playervaults/listeners/Listeners.java +++ b/src/main/java/com/drtshock/playervaults/listeners/Listeners.java @@ -227,4 +227,4 @@ private boolean isBlocked(Player player, ItemStack item, VaultViewInfo info) { } return false; } -} \ No newline at end of file +} diff --git a/src/main/java/com/drtshock/playervaults/listeners/VaultPreloadListener.java b/src/main/java/com/drtshock/playervaults/listeners/VaultPreloadListener.java index 14467a0..d1136f8 100644 --- a/src/main/java/com/drtshock/playervaults/listeners/VaultPreloadListener.java +++ b/src/main/java/com/drtshock/playervaults/listeners/VaultPreloadListener.java @@ -25,7 +25,6 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.scheduler.BukkitRunnable; import java.util.UUID; @@ -37,12 +36,7 @@ public class VaultPreloadListener implements Listener { public void onPlayerJoin(PlayerJoinEvent event) { PlayerVaults.getInstance().updateNotification(event.getPlayer()); final UUID uuid = event.getPlayer().getUniqueId(); - new BukkitRunnable() { - @Override - public void run() { - vm.cachePlayerVaultFile(uuid.toString()); - } - }.runTaskAsynchronously(PlayerVaults.getInstance()); + PlayerVaults.scheduler().runAsync(task -> vm.cachePlayerVaultFile(uuid.toString())); } @EventHandler(priority = EventPriority.MONITOR) diff --git a/src/main/java/com/drtshock/playervaults/vaultmanagement/EconomyOperations.java b/src/main/java/com/drtshock/playervaults/vaultmanagement/EconomyOperations.java index 8418ada..5f1b1e0 100644 --- a/src/main/java/com/drtshock/playervaults/vaultmanagement/EconomyOperations.java +++ b/src/main/java/com/drtshock/playervaults/vaultmanagement/EconomyOperations.java @@ -123,7 +123,7 @@ public static boolean refundOnDelete(Player player, int number) { return true; } - File playerFile = new File(PlayerVaults.getInstance().getVaultData(), player.getUniqueId().toString() + ".yml"); + File playerFile = new File(PlayerVaults.getInstance().getVaultData(), player.getUniqueId() + ".yml"); if (playerFile.exists()) { YamlConfiguration playerData = YamlConfiguration.loadConfiguration(playerFile); if (playerData.getString("vault" + number) == null) { diff --git a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultManager.java b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultManager.java index 742ac8c..6656edc 100644 --- a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultManager.java +++ b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultManager.java @@ -27,17 +27,21 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; -import org.bukkit.scheduler.BukkitRunnable; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; +import static com.drtshock.playervaults.vaultmanagement.VaultOperations.VaultGate; + public class VaultManager { private static final String VAULTKEY = "vault%d"; @@ -60,6 +64,35 @@ public static VaultManager getInstance() { return instance; } + /** + * Resolve a stable, UUID-first key for a holder. + */ + public static String normalizeHolderKey(String input) { + if (input == null) { + return null; + } + + File dataDir = PlayerVaults.getInstance().getVaultData(); + File legacy = new File(dataDir, input + ".yml"); + if (legacy.exists()) { + return input; + } + + try { + UUID uuid = UUID.fromString(input); + return uuid.toString(); + } catch (Exception ignored) { + // Ignored case/handler. + } + + OfflinePlayer byName = Bukkit.getOfflinePlayer(input); + if (byName != null && byName.getUniqueId() != null) { + return byName.getUniqueId().toString(); + } + + return input; + } + /** * Saves the inventory to the specified player and vault number. * @@ -68,11 +101,14 @@ public static VaultManager getInstance() { * @param number The vault number. */ public void saveVault(Inventory inventory, String target, int number) { - YamlConfiguration yaml = getPlayerVaultFile(target, true); - int size = VaultOperations.getMaxVaultSize(target); - String serialized = CardboardBoxSerialization.toStorage(inventory, target); - yaml.set(String.format(VAULTKEY, number), serialized); - saveFileSync(target, yaml); + final String holderKey = normalizeHolderKey(target); + VaultGate.withLock(new VaultGate.VaultKey(holderKey, number), () -> { + YamlConfiguration yaml = getPlayerVaultFile(holderKey, true); + VaultOperations.getMaxVaultSize(holderKey); + String serialized = CardboardBoxSerialization.toStorage(inventory, holderKey); + yaml.set(String.format(VAULTKEY, number), serialized); + saveFileSync(holderKey, yaml); + }); } /** @@ -118,28 +154,19 @@ public Inventory loadOtherVault(String name, int number, int size) { size = PlayerVaults.getInstance().getDefaultVaultSize(); } - PlayerVaults.debug("Loading other vault for " + name); - - String holder = name; - - try { - UUID uuid = UUID.fromString(name); - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid); - holder = offlinePlayer.getUniqueId().toString(); - } catch (Exception e) { - // Not a player - } + final String holderKey = normalizeHolderKey(name); + PlayerVaults.debug("Loading other vault for " + holderKey); String title = PlayerVaults.getInstance().getVaultTitle(String.valueOf(number)); - VaultViewInfo info = new VaultViewInfo(name, number); + VaultViewInfo info = new VaultViewInfo(holderKey, number); Inventory inv; VaultHolder vaultHolder = new VaultHolder(number); if (PlayerVaults.getInstance().getOpenInventories().containsKey(info.toString())) { PlayerVaults.debug("Already open"); inv = PlayerVaults.getInstance().getOpenInventories().get(info.toString()); } else { - YamlConfiguration playerFile = getPlayerVaultFile(holder, true); - Inventory i = getInventory(vaultHolder, holder, playerFile, size, number, title); + YamlConfiguration playerFile = getPlayerVaultFile(holderKey, true); + Inventory i = getInventory(vaultHolder, holderKey, playerFile, size, number, title); if (i == null) { return null; } else { @@ -172,7 +199,7 @@ private Inventory getInventory(InventoryHolder owner, String ownerName, YamlConf // Happens on change of permission or if people used the broken version. // In this case, players will lose items. if (deserialized.length > size) { - PlayerVaults.debug("Loaded vault for " + ownerName + " and got " + deserialized.length + " items for allowed size of " + size+". Attempting to rescue!"); + PlayerVaults.debug("Loaded vault for " + ownerName + " and got " + deserialized.length + " items for allowed size of " + size + ". Attempting to rescue!"); for (ItemStack stack : deserialized) { if (stack != null) { inventory.addItem(stack); @@ -194,11 +221,14 @@ private Inventory getInventory(InventoryHolder owner, String ownerName, YamlConf * @return The inventory of the specified holder and vault number. Can be null. */ public Inventory getVault(String holder, int number) { - YamlConfiguration playerFile = getPlayerVaultFile(holder, true); + String holderKey = normalizeHolderKey(holder); + YamlConfiguration playerFile = getPlayerVaultFile(holderKey, true); String serialized = playerFile.getString(String.format(VAULTKEY, number)); - ItemStack[] contents = CardboardBoxSerialization.fromStorage(serialized, holder); - Inventory inventory = Bukkit.createInventory(null, contents.length, holder + " vault " + number); - inventory.setContents(contents); + ItemStack[] contents = CardboardBoxSerialization.fromStorage(serialized, holderKey); + int size = Math.max(9, ((contents.length + 8) / 9) * 9); + Inventory inventory = Bukkit.createInventory(null, size, holderKey + " vault " + number); + ItemStack[] copy = Arrays.copyOf(contents, size); + inventory.setContents(copy); return inventory; } @@ -210,12 +240,13 @@ public Inventory getVault(String holder, int number) { * @return true if the vault file and vault number exist in that file, otherwise false. */ public boolean vaultExists(String holder, int number) { - File file = new File(directory, holder + ".yml"); + String holderKey = normalizeHolderKey(holder); + File file = new File(directory, holderKey + ".yml"); if (!file.exists()) { return false; } - return getPlayerVaultFile(holder, true).contains(String.format(VAULTKEY, number)); + return getPlayerVaultFile(holderKey, true).contains(String.format(VAULTKEY, number)); } /** @@ -226,7 +257,8 @@ public boolean vaultExists(String holder, int number) { */ public Set getVaultNumbers(String holder) { Set vaults = new HashSet<>(); - YamlConfiguration file = getPlayerVaultFile(holder, true); + String holderKey = normalizeHolderKey(holder); + YamlConfiguration file = getPlayerVaultFile(holderKey, true); if (file == null) { return vaults; } @@ -241,13 +273,32 @@ public Set getVaultNumbers(String holder) { } } - return vaults; } public void deleteAllVaults(String holder) { - removeCachedPlayerVaultFile(holder); - deletePlayerVaultFile(holder); + String holderKey = normalizeHolderKey(holder); + removeCachedPlayerVaultFile(holderKey); + deletePlayerVaultFile(holderKey); + + List toRemove = new ArrayList<>(); + PlayerVaults.getInstance().getInVault().forEach((viewerId, info) -> { + if (holderKey.equals(info.getVaultName())) { + toRemove.add(viewerId); + Player p = null; + try { + p = Bukkit.getPlayer(UUID.fromString(viewerId)); + } catch (Exception ignored) { + // Ignored try actionable. + } + if (p != null) { + Player finalP = p; + PlayerVaults.scheduler().runAtEntity(finalP, task -> finalP.closeInventory()); + } + PlayerVaults.getInstance().getOpenInventories().remove(info.toString()); + } + }); + toRemove.forEach(id -> PlayerVaults.getInstance().getInVault().remove(id)); } /** @@ -258,51 +309,82 @@ public void deleteAllVaults(String holder) { * @param number The vault number. */ public void deleteVault(CommandSender sender, final String holder, final int number) { - new BukkitRunnable() { - @Override - public void run() { - File file = new File(directory, holder + ".yml"); + final String holderKey = normalizeHolderKey(holder); + final VaultGate.VaultKey gateKey = new VaultGate.VaultKey(holderKey, number); + + PlayerVaults.scheduler().runAsync(task -> + VaultGate.withLock(gateKey, () -> { + File file = new File(directory, holderKey + ".yml"); if (!file.exists()) { return; } YamlConfiguration playerFile = YamlConfiguration.loadConfiguration(file); - if (file.exists()) { - playerFile.set(String.format(VAULTKEY, number), null); - if (cachedVaultFiles.containsKey(holder)) { - cachedVaultFiles.put(holder, playerFile); - } - try { - playerFile.save(file); - } catch (IOException ignored) { - } + playerFile.set(String.format(VAULTKEY, number), null); + cachedVaultFiles.put(holderKey, playerFile); + try { + playerFile.save(file); + } catch (IOException ignored) { } - } - }.runTaskAsynchronously(PlayerVaults.getInstance()); - OfflinePlayer player = Bukkit.getPlayer(holder); - if (player != null) { - if (sender.getName().equalsIgnoreCase(player.getName())) { + String key = new VaultViewInfo(holderKey, number).toString(); + PlayerVaults.getInstance().getOpenInventories().remove(key); + + List toRemove = new ArrayList<>(); + PlayerVaults.getInstance().getInVault().forEach((viewerId, info) -> { + if (holderKey.equals(info.getVaultName()) && info.getNumber() == number) { + toRemove.add(viewerId); + Player p = null; + try { + p = Bukkit.getPlayer(UUID.fromString(viewerId)); + } catch (Exception ignored) { + // Ignored try actionable. + } + if (p != null) { + Player finalP = p; + PlayerVaults.scheduler().runAtEntity(finalP, t -> finalP.closeInventory()); + } + } + }); + toRemove.forEach(id -> PlayerVaults.getInstance().getInVault().remove(id)); + }) + ); + + OfflinePlayer target = null; + try { + target = Bukkit.getOfflinePlayer(UUID.fromString(holderKey)); + } catch (Exception ignored) { + // Ignored try actionable. + } + if (target != null && target.getName() != null) { + if (sender.getName().equalsIgnoreCase(target.getName())) { + this.plugin.getTL().deleteVault().title().with("vault", String.valueOf(number)).send(sender); + } else { + this.plugin.getTL().deleteOtherVault().title().with("vault", String.valueOf(number)).with("player", target.getName()).send(sender); + } + } else { + if (sender.getName().equalsIgnoreCase(holder)) { this.plugin.getTL().deleteVault().title().with("vault", String.valueOf(number)).send(sender); } else { - this.plugin.getTL().deleteOtherVault().title().with("vault", String.valueOf(number)).with("player", player.getName()).send(sender); + this.plugin.getTL().deleteOtherVault().title().with("vault", String.valueOf(number)).with("player", holder).send(sender); } } - String vaultName = sender instanceof Player ? ((Player) sender).getUniqueId().toString() : holder; - PlayerVaults.getInstance().getOpenInventories().remove(new VaultViewInfo(vaultName, number).toString()); + PlayerVaults.getInstance().getOpenInventories().remove(new VaultViewInfo(holderKey, number).toString()); } // Should only be run asynchronously public void cachePlayerVaultFile(String holder) { - YamlConfiguration config = this.loadPlayerVaultFile(holder, false); + String holderKey = normalizeHolderKey(holder); + YamlConfiguration config = this.loadPlayerVaultFile(holderKey, false); if (config != null) { - this.cachedVaultFiles.put(holder, config); + this.cachedVaultFiles.put(holderKey, config); } } public void removeCachedPlayerVaultFile(String holder) { - cachedVaultFiles.remove(holder); + String holderKey = normalizeHolderKey(holder); + cachedVaultFiles.remove(holderKey); } /** @@ -312,10 +394,8 @@ public void removeCachedPlayerVaultFile(String holder) { * @return The holder's vault config file. */ public YamlConfiguration getPlayerVaultFile(String holder, boolean createIfNotFound) { - if (cachedVaultFiles.containsKey(holder)) { - return cachedVaultFiles.get(holder); - } - return loadPlayerVaultFile(holder, createIfNotFound); + String holderKey = resolveFileKey(holder); + return cachedVaultFiles.computeIfAbsent(holderKey, key -> loadPlayerVaultFile(key, createIfNotFound)); } public YamlConfiguration loadPlayerVaultFile(String holder) { @@ -328,7 +408,8 @@ public YamlConfiguration loadPlayerVaultFile(String holder) { * @param holder UUID of the holder. */ public void deletePlayerVaultFile(String holder) { - File file = new File(this.directory, holder + ".yml"); + String holderKey = resolveFileKey(holder); + File file = new File(this.directory, holderKey + ".yml"); if (file.exists()) { file.delete(); } @@ -356,22 +437,38 @@ public YamlConfiguration loadPlayerVaultFile(String uniqueId, boolean createIfNo } public void saveFileSync(final String holder, final YamlConfiguration yaml) { - if (cachedVaultFiles.containsKey(holder)) { - cachedVaultFiles.put(holder, yaml); + String holderKey = resolveFileKey(holder); + if (cachedVaultFiles.containsKey(holderKey)) { + cachedVaultFiles.put(holderKey, yaml); } final boolean backups = PlayerVaults.getInstance().isBackupsEnabled(); final File backupsFolder = PlayerVaults.getInstance().getBackupsFolder(); - final File file = new File(directory, holder + ".yml"); + final File file = new File(directory, holderKey + ".yml"); if (file.exists() && backups) { - file.renameTo(new File(backupsFolder, holder + ".yml")); + file.renameTo(new File(backupsFolder, holderKey + ".yml")); } + try { yaml.save(file); } catch (IOException e) { - PlayerVaults.getInstance().addException(new IllegalStateException("Failed to save vault file for: " + holder, e)); - PlayerVaults.getInstance().getLogger().log(Level.SEVERE, "Failed to save vault file for: " + holder, e); + PlayerVaults.getInstance().addException(new IllegalStateException("Failed to save vault file for: " + holderKey, e)); + PlayerVaults.getInstance().getLogger().log(Level.SEVERE, "Failed to save vault file for: " + holderKey, e); } - PlayerVaults.debug("Saved vault for " + holder); + + PlayerVaults.debug("Saved vault for " + holderKey); + } + + private String resolveFileKey(String holder) { + if (holder == null) { + return null; + } + + File legacy = new File(this.directory, holder + ".yml"); + if (legacy.exists()) { + return holder; + } + + return normalizeHolderKey(holder); } } diff --git a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultOperations.java b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultOperations.java index 9134795..c844cd9 100644 --- a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultOperations.java +++ b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultOperations.java @@ -34,11 +34,48 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; public class VaultOperations { private static final AtomicBoolean LOCKED = new AtomicBoolean(false); + public static final class VaultGate { + + private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>(); + + public static T withLock(VaultKey key, Supplier body) { + ReentrantLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantLock()); + lock.lock(); + try { + return body.get(); + } finally { + try { + lock.unlock(); + } finally { + if (!lock.hasQueuedThreads()) { + LOCKS.remove(key, lock); + } + } + } + } + + public static void withLock(VaultKey key, Runnable body) { + withLock(key, () -> { + body.run(); + return null; + }); + } + + public record VaultKey(String ownerKey, int number) { + @Override + public String toString() { + return ownerKey + " " + number; + } + } + } + /** * Gets whether or not player vaults are locked * @@ -59,12 +96,10 @@ public static void setLocked(boolean locked) { if (locked) { for (Player player : PlayerVaults.getInstance().getServer().getOnlinePlayers()) { - if (player.getOpenInventory() != null) { - InventoryView view = player.getOpenInventory(); - if (view.getTopInventory().getHolder() instanceof VaultHolder) { - player.closeInventory(); - PlayerVaults.getInstance().getTL().locked().title().send(player); - } + InventoryView view = player.getOpenInventory(); + if (view != null && view.getTopInventory() != null && view.getTopInventory().getHolder() instanceof VaultHolder) { + PlayerVaults.scheduler().runAtEntity(player, task -> player.closeInventory()); + PlayerVaults.getInstance().getTL().locked().title().send(player); } } } @@ -143,7 +178,7 @@ private static boolean openOwnVaultE(Player player, String arg, boolean free, bo if (player.isSleeping() || player.isDead() || !player.isOnline()) { return false; } - int number; + final int number; try { number = Integer.parseInt(arg); if (number < 1) { @@ -154,37 +189,46 @@ private static boolean openOwnVaultE(Player player, String arg, boolean free, bo return false; } - if (checkPerms(player, number)) { - if (free || EconomyOperations.payToOpen(player, number)) { - Inventory inv = VaultManager.getInstance().loadOwnVault(player, number, getMaxVaultSize(player)); - if (inv == null) { - PlayerVaults.debug(String.format("Failed to open null vault %d for %s. This is weird.", number, player.getName())); - return false; - } + if (!checkPerms(player, number)) { + PlayerVaults.getInstance().getTL().noPerms().title().send(player); + return false; + } - player.openInventory(inv); + if (!free && !EconomyOperations.payToOpen(player, number)) { + PlayerVaults.getInstance().getTL().insufficientFunds().title().send(player); + return false; + } - // Check if the inventory was actually opened - if (player.getOpenInventory().getTopInventory() instanceof CraftingInventory || player.getOpenInventory().getTopInventory() == null) { - PlayerVaults.debug(String.format("Cancelled opening vault %s for %s from an outside source.", arg, player.getName())); - return false; // inventory open event was cancelled. - } + final String ownerKey = player.getUniqueId().toString(); + final VaultViewInfo info = new VaultViewInfo(ownerKey, number); + final VaultGate.VaultKey gateKey = new VaultGate.VaultKey(ownerKey, number); - VaultViewInfo info = new VaultViewInfo(player.getUniqueId().toString(), number); - PlayerVaults.getInstance().getOpenInventories().put(info.toString(), inv); + Inventory inv = VaultGate.withLock(gateKey, () -> + PlayerVaults.getInstance().getOpenInventories().computeIfAbsent(info.toString(), key -> + VaultManager.getInstance().loadOwnVault(player, number, getMaxVaultSize(player)) + ) + ); - if (send) { - PlayerVaults.getInstance().getTL().openVault().title().with("vault", arg).send(player); - } - return true; - } else { - PlayerVaults.getInstance().getTL().insufficientFunds().title().send(player); - return false; - } + if (inv == null) { + PlayerVaults.debug(String.format("Failed to open null vault %d for %s. This is weird.", number, player.getName())); + return false; + } + + PlayerVaults.scheduler().runAtEntity(player, task -> player.openInventory(inv)); + + // Check if the inventory was actually opened + if (player.getOpenInventory().getTopInventory() instanceof CraftingInventory) { + PlayerVaults.debug(String.format("Cancelled opening vault %s for %s from an outside source.", arg, player.getName())); + PlayerVaults.getInstance().getOpenInventories().remove(info.toString()); + return false; // inventory open event was cancelled. } else { - PlayerVaults.getInstance().getTL().noPerms().title().send(player); + player.getOpenInventory().getTopInventory(); } - return false; + + if (send) { + PlayerVaults.getInstance().getTL().openVault().title().with("vault", arg).send(player); + } + return true; } /** @@ -207,7 +251,7 @@ public static boolean openOwnVault(Player player, String arg, boolean isCommand) * Open another player's vault. * * @param player The player to open to. - * @param vaultOwner The name of the vault owner. + * @param vaultOwner The name or UUID of the vault owner. * @param arg The vault number to open. * @return Whether or not the player was allowed to open it. */ @@ -224,9 +268,7 @@ public static boolean openOtherVault(Player player, String vaultOwner, String ar return false; } - long time = System.currentTimeMillis(); - - int number = 0; + final int number; try { number = Integer.parseInt(arg); if (number < 1) { @@ -235,41 +277,63 @@ public static boolean openOtherVault(Player player, String vaultOwner, String ar } } catch (NumberFormatException nfe) { PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); + return false; } - Inventory inv = VaultManager.getInstance().loadOtherVault(vaultOwner, number, getMaxVaultSize(vaultOwner)); - String name = vaultOwner; + final String holderKey = VaultManager.normalizeHolderKey(vaultOwner); + final VaultViewInfo info = new VaultViewInfo(holderKey, number); + final VaultGate.VaultKey gateKey = new VaultGate.VaultKey(holderKey, number); + + long time = System.currentTimeMillis(); + + Inventory inv = VaultGate.withLock(gateKey, () -> { + Inventory cached = PlayerVaults.getInstance().getOpenInventories().get(info.toString()); + if (cached != null) { + return cached; + } + Inventory loaded = VaultManager.getInstance().loadOtherVault(holderKey, number, getMaxVaultSize(holderKey)); + if (loaded != null) { + PlayerVaults.getInstance().getOpenInventories().put(info.toString(), loaded); + } + return loaded; + }); + + // Resolve a nice display name if possible + String displayName = holderKey; try { - OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(UUID.fromString(vaultOwner)); - name = offlinePlayer.getName(); + UUID uuid = UUID.fromString(holderKey); + OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + if (op.getName() != null) { + displayName = op.getName(); + } } catch (Exception e) { // not a player } if (inv == null) { PlayerVaults.getInstance().getTL().vaultDoesNotExist().title().send(player); - } else { - player.openInventory(inv); + PlayerVaults.debug("Opening other vault that does not fully exist.", time); + return false; + } - // Check if the inventory was actually opened - if (player.getOpenInventory().getTopInventory() instanceof CraftingInventory || player.getOpenInventory().getTopInventory() == null) { - PlayerVaults.debug(String.format("Cancelled opening vault %s for %s from an outside source.", arg, player.getName())); - return false; // inventory open event was cancelled. - } - if (send) { - PlayerVaults.getInstance().getTL().openOtherVault().title().with("vault", arg).with("player", name).send(player); - } - PlayerVaults.debug("opening other vault", time); + PlayerVaults.scheduler().runAtEntity(player, task -> player.openInventory(inv)); - // Need to set ViewInfo for a third party vault for the opening player. - VaultViewInfo info = new VaultViewInfo(vaultOwner, number); - PlayerVaults.getInstance().getInVault().put(player.getUniqueId().toString(), info); - PlayerVaults.getInstance().getOpenInventories().put(player.getUniqueId().toString(), inv); - return true; + // Check if the inventory was actually opened + if (player.getOpenInventory().getTopInventory() instanceof CraftingInventory) { + PlayerVaults.debug(String.format("Cancelled opening vault %s for %s from an outside source.", arg, player.getName())); + return false; // inventory open event was cancelled. + } else { + player.getOpenInventory().getTopInventory(); } - PlayerVaults.debug("opening other vault returning false", time); - return false; + if (send) { + PlayerVaults.getInstance().getTL().openOtherVault().title().with("vault", arg).with("player", displayName).send(player); + } + PlayerVaults.debug("opening other vault", time); + + // Track which vault this viewer is in + PlayerVaults.getInstance().getInVault().put(player.getUniqueId().toString(), info); + return true; } /** @@ -282,25 +346,27 @@ public static void deleteOwnVault(Player player, String arg) { if (isLocked()) { return; } - if (isNumber(arg)) { - int number = 0; - try { - number = Integer.parseInt(arg); - if (number == 0) { - PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); - return; - } - } catch (NumberFormatException nfe) { - PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); - } - if (EconomyOperations.refundOnDelete(player, number)) { - VaultManager.getInstance().deleteVault(player, player.getUniqueId().toString(), number); - PlayerVaults.getInstance().getTL().deleteVault().title().with("vault", arg).send(player); - } + if (!isNumber(arg)) { + PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); + return; + } - } else { + final int number; + try { + number = Integer.parseInt(arg); + if (number == 0) { + PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); + return; + } + } catch (NumberFormatException nfe) { PlayerVaults.getInstance().getTL().mustBeNumber().title().send(player); + return; + } + + if (EconomyOperations.refundOnDelete(player, number)) { + VaultManager.getInstance().deleteVault(player, player.getUniqueId().toString(), number); + PlayerVaults.getInstance().getTL().deleteVault().title().with("vault", arg).send(player); } } @@ -315,27 +381,37 @@ public static void deleteOtherVault(CommandSender sender, String holder, String if (isLocked()) { return; } - if (sender.hasPermission(Permission.DELETE)) { - if (isNumber(arg)) { - int number = 0; - try { - number = Integer.parseInt(arg); - if (number == 0) { - PlayerVaults.getInstance().getTL().mustBeNumber().title().send(sender); - return; - } - } catch (NumberFormatException nfe) { - PlayerVaults.getInstance().getTL().mustBeNumber().title().send(sender); - } + if (!sender.hasPermission(Permission.DELETE)) { + PlayerVaults.getInstance().getTL().noPerms().title().send(sender); + return; + } + if (!isNumber(arg)) { + PlayerVaults.getInstance().getTL().mustBeNumber().title().send(sender); + return; + } - VaultManager.getInstance().deleteVault(sender, holder, number); - PlayerVaults.getInstance().getTL().deleteOtherVault().title().with("vault", arg).with("player", holder).send(sender); - } else { + final int number; + try { + number = Integer.parseInt(arg); + if (number == 0) { PlayerVaults.getInstance().getTL().mustBeNumber().title().send(sender); + return; } - } else { - PlayerVaults.getInstance().getTL().noPerms().title().send(sender); + } catch (NumberFormatException nfe) { + PlayerVaults.getInstance().getTL().mustBeNumber().title().send(sender); + return; + } + + VaultManager.getInstance().deleteVault(sender, holder, number); + String display = holder; + try { + UUID uuid = UUID.fromString(holder); + OfflinePlayer op = Bukkit.getOfflinePlayer(uuid); + if (op.getName() != null) display = op.getName(); + } catch (Exception e) { + // The return instance here can be suppressed } + PlayerVaults.getInstance().getTL().deleteOtherVault().title().with("vault", arg).with("player", display).send(sender); } /** @@ -369,15 +445,15 @@ private static boolean isNumber(String check) { private record PlayerCount(int count, Instant time) { } - private static Map countCache = new ConcurrentHashMap<>(); + private static final Map countCache = new ConcurrentHashMap<>(); private static final int secondsToLive = 2; public static int countVaults(Player player) { UUID uuid = player.getUniqueId(); - PlayerCount count = countCache.get(uuid); - if (count != null && count.time().isAfter(Instant.now().plus(secondsToLive, ChronoUnit.SECONDS))) { - return count.count; + PlayerCount cached = countCache.get(uuid); + if (cached != null && Instant.now().isBefore(cached.time().plus(secondsToLive, ChronoUnit.SECONDS))) { + return cached.count; } int vaultCount = 0; for (int x = 1; x <= PlayerVaults.getInstance().getMaxVaultAmountPermTest(); x++) { @@ -387,7 +463,7 @@ public static int countVaults(Player player) { } PlayerCount newCount = new PlayerCount(vaultCount, Instant.now()); countCache.put(uuid, newCount); - PlayerVaults.getInstance().getServer().getScheduler().runTaskLater(PlayerVaults.getInstance(), () -> { + PlayerVaults.scheduler().runLater(() -> { if (countCache.get(uuid) == newCount) { countCache.remove(uuid); // Do a lil cleanup to avoid the world's smallest memory leak } diff --git a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultViewInfo.java b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultViewInfo.java index 756b9d2..0dbf646 100644 --- a/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultViewInfo.java +++ b/src/main/java/com/drtshock/playervaults/vaultmanagement/VaultViewInfo.java @@ -29,6 +29,7 @@ public class VaultViewInfo { /** * Makes a VaultViewInfo object. Used for opening a vault owned by the opener. * + * @param vaultName UUID (string) or legacy key * @param i vault number. */ public VaultViewInfo(String vaultName, int i) { @@ -58,4 +59,4 @@ public int getNumber() { public String toString() { return this.vaultName + " " + this.number; } -} \ No newline at end of file +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 5fb647b..ab264e8 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -6,6 +6,7 @@ version: ${project.version} main: com.drtshock.playervaults.PlayerVaults softdepend: [Vault, Multiverse-Inventories, PlaceholderAPI] api-version: 1.21.10 +folia-supported: true commands: pv: