diff --git a/api/src/main/java/com/lishid/openinv/IOpenInv.java b/api/src/main/java/com/lishid/openinv/IOpenInv.java index 64697ff..5b12e6a 100644 --- a/api/src/main/java/com/lishid/openinv/IOpenInv.java +++ b/api/src/main/java/com/lishid/openinv/IOpenInv.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,15 +42,23 @@ import org.jetbrains.annotations.Nullable; public interface IOpenInv { /** - * Check the configuration value for whether or not OpenInv saves player data when unloading - * players. This is exclusively for users who do not allow editing of inventories, only viewing, - * and wish to prevent any possibility of bugs such as lishid#40. If true, OpenInv will not ever - * save any edits made to players. + * Check the configuration value for whether OpenInv saves player data when unloading players. This is exclusively + * for users who do not allow editing of inventories, only viewing, and wish to prevent any possibility of bugs such + * as lishid#40. If true, OpenInv will not ever save any edits made to players. * * @return false unless configured otherwise */ boolean disableSaving(); + /** + * Check the configuration value for whether OpenInv allows offline access. This does not prevent other plugins from + * using existing loaded players while offline. + * + * @return false unless configured otherwise + * @since 4.2.0 + */ + boolean disableOfflineAccess(); + /** * Gets the active ISilentContainer implementation. * @@ -60,12 +68,9 @@ public interface IOpenInv { @NotNull IAnySilentContainer getAnySilentContainer(); /** - * Gets the active IInventoryAccess implementation. - * - * @return the IInventoryAccess - * @throws IllegalStateException if the server version is unsupported + * @deprecated Use static {@link InventoryAccess} methods. */ - @Deprecated + @Deprecated(forRemoval = true) default @NotNull IInventoryAccess getInventoryAccess() { return new InventoryAccess(); } @@ -129,6 +134,15 @@ public interface IOpenInv { */ boolean isSupportedVersion(); + /** + * Check if a {@link Player} is currently loaded by OpenInv. + * + * @param playerUuid the {@link UUID} of the {@code Player} + * @return whether the {@code Player} is loaded + * @since 4.2.0 + */ + boolean isPlayerLoaded(@NotNull UUID playerUuid); + /** * Load a Player from an OfflinePlayer. May return null under some circumstances. * @@ -227,57 +241,51 @@ public interface IOpenInv { * @return true unless configured otherwise * @deprecated OpenInv uses action bar chat for notifications. Whether or not they show is based on language settings. */ - @Deprecated + @Deprecated(forRemoval = true) default boolean notifyAnyChest() { return true; } /** - * Check the configuration value for whether or not OpenInv displays a notification to the user - * when a container is activated with SilentChest. - * - * @return true unless configured otherwise - * @deprecated OpenInv uses action bar chat for notifications. Whether or not they show is based on language settings. + * @deprecated OpenInv uses action bar chat for notifications. Whether they show is based on language settings. */ - @Deprecated + @Deprecated(forRemoval = true) default boolean notifySilentChest() { return true; } /** - * Mark a Player as no longer in use by a Plugin to allow OpenInv to remove it from the cache - * when eligible. - * - * @param player the Player - * @param plugin the Plugin no longer holding a reference to the Player - * @throws IllegalStateException if the server version is unsupported + * @deprecated see {@link #retainPlayer(Player, Plugin)} */ - void releasePlayer(@NotNull Player player, @NotNull Plugin plugin); + @Deprecated(forRemoval = true, since = "4.2.0") + default void releasePlayer(@NotNull Player player, @NotNull Plugin plugin) {} /** - * Mark a Player as in use by a Plugin to prevent it from being removed from the cache. Used to - * prevent issues with multiple copies of the same Player being loaded such as lishid#49. - * Changes made to loaded copies overwrite changes to the others when saved, leading to - * duplication bugs and more. - *

- * When finished with the Player object, be sure to call {@link #releasePlayer(Player, Plugin)} - * to prevent the cache from keeping it stored until the plugin is disabled. - *

- * When using a Player object from OpenInv, you must handle the Player coming online, replacing - * your Player reference with the Player from the PlayerJoinEvent. In addition, you must change - * any values in the Player to reflect any unsaved alterations to the existing Player which do - * not affect the inventory or ender chest contents. - *

- * OpenInv only saves player data when unloading a Player from the cache, and then only if - * {@link #disableSaving()} returns false. If you are making changes that OpenInv does not cause - * to persist when a Player logs in as noted above, it is suggested that you manually call - * {@link Player#saveData()} when releasing your reference to ensure your changes persist. + * @deprecated OpenInv no longer uses an internal cache beyond maintaining copies of currently open inventories. + * If you wish to use/modify a player, ensure either {@link IOpenInv#isPlayerLoaded(UUID)} is false or the player + * instance is the same memory address as the one in use by OpenInv. + *

+     *  public @NotNull Player savePlayerData(@NotNull Player player) {
+     *     IOpenInv openInv = ...
+     *     if (!openInv.disableSaving() && openInv.isPlayerLoaded(player.getUniqueId())) {
+     *         Player openInvLoadedPlayer = openInv.loadPlayer(myInUsePlayer);
+     *         if (openInvLoadedPlayer != player) {
+     *             // The copy loaded by OpenInv is not the same as our loaded copy. Push our changes.
+     *             copyPlayerModifications(player, openInvLoadedPlayer);
+     *         }
+     *         // OpenInv will handle saving data when the player is unloaded.
+     *         // Optionally, to be sure our changes will persist, save now.
+     *         // openInvLoadedPlayer.saveData();
+     *         return openInvLoadedPlayer;
+     *     }
      *
-     * @param player the Player
-     * @param plugin the Plugin holding the reference to the Player
-     * @throws IllegalStateException if the server version is unsupported
+     *     player.saveData();
+     *     return player;
+     * }
+     * 
*/ - void retainPlayer(@NotNull Player player, @NotNull Plugin plugin); + @Deprecated(forRemoval = true, since = "4.2.0") + default void retainPlayer(@NotNull Player player, @NotNull Plugin plugin) {} /** * Sets a player's AnyChest setting. diff --git a/plugin/src/main/java/com/lishid/openinv/InventoryListener.java b/plugin/src/main/java/com/lishid/openinv/InventoryListener.java index 8e1c27e..1a6a799 100644 --- a/plugin/src/main/java/com/lishid/openinv/InventoryListener.java +++ b/plugin/src/main/java/com/lishid/openinv/InventoryListener.java @@ -16,6 +16,7 @@ package com.lishid.openinv; +import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; import com.lishid.openinv.util.InventoryAccess; import com.lishid.openinv.util.Permissions; @@ -47,7 +48,7 @@ import org.jetbrains.annotations.Nullable; record InventoryListener(OpenInv plugin) implements Listener { @EventHandler - public void onInventoryClose(@NotNull final InventoryCloseEvent event) { + private void onInventoryClose(@NotNull final InventoryCloseEvent event) { if (!(event.getPlayer() instanceof Player player)) { return; } @@ -55,10 +56,20 @@ record InventoryListener(OpenInv plugin) implements Listener { if (this.plugin.getPlayerSilentChestStatus(player)) { this.plugin.getAnySilentContainer().deactivateContainer(player); } + + ISpecialInventory specialInventory = InventoryAccess.getEnderChest(event.getInventory()); + if (specialInventory != null) { + this.plugin.handleCloseInventory(event.getPlayer(), specialInventory); + } else { + specialInventory = InventoryAccess.getPlayerInventory(event.getInventory()); + if (specialInventory != null) { + this.plugin.handleCloseInventory(event.getPlayer(), specialInventory); + } + } } @EventHandler(priority = EventPriority.LOWEST) - public void onInventoryClick(@NotNull final InventoryClickEvent event) { + private void onInventoryClick(@NotNull final InventoryClickEvent event) { if (handleInventoryInteract(event)) { return; } @@ -92,7 +103,7 @@ record InventoryListener(OpenInv plugin) implements Listener { } @EventHandler(priority = EventPriority.LOWEST) - public void onInventoryDrag(@NotNull final InventoryDragEvent event) { + private void onInventoryDrag(@NotNull final InventoryDragEvent event) { if (handleInventoryInteract(event)) { return; } diff --git a/plugin/src/main/java/com/lishid/openinv/OfflineHandler.java b/plugin/src/main/java/com/lishid/openinv/OfflineHandler.java new file mode 100644 index 0000000..ea0e8c3 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/OfflineHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv; + +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.util.Permissions; +import java.util.Map; +import java.util.UUID; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.jetbrains.annotations.NotNull; + +record OfflineHandler( + @NotNull BiFunction, UUID, ISpecialInventory> fetch, + @NotNull Consumer<@NotNull ISpecialInventory> handle) { + + static final OfflineHandler REMOVE_AND_CLOSE = new OfflineHandler( + Map::remove, + inventory -> OpenInv.ejectViewers(inventory, viewer -> true) + ); + + static final OfflineHandler REQUIRE_PERMISSIONS = new OfflineHandler( + Map::get, + inventory -> OpenInv.ejectViewers(inventory, viewer -> !Permissions.OPENOFFLINE.hasPermission(viewer)) + ); + +} diff --git a/plugin/src/main/java/com/lishid/openinv/OpenInv.java b/plugin/src/main/java/com/lishid/openinv/OpenInv.java index 3ad4f5c..5e3591b 100644 --- a/plugin/src/main/java/com/lishid/openinv/OpenInv.java +++ b/plugin/src/main/java/com/lishid/openinv/OpenInv.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,8 +16,6 @@ package com.lishid.openinv; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; import com.lishid.openinv.commands.ContainerSettingCommand; import com.lishid.openinv.commands.OpenInvCommand; import com.lishid.openinv.commands.SearchContainerCommand; @@ -27,23 +25,19 @@ import com.lishid.openinv.internal.IAnySilentContainer; import com.lishid.openinv.internal.ISpecialEnderChest; import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; -import com.lishid.openinv.listeners.InventoryListener; -import com.lishid.openinv.listeners.PlayerListener; -import com.lishid.openinv.listeners.PluginListener; -import com.lishid.openinv.util.Cache; import com.lishid.openinv.util.ConfigUpdater; -import com.lishid.openinv.util.InternalAccessor; import com.lishid.openinv.util.LanguageManager; import com.lishid.openinv.util.Permissions; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.TextComponent; import org.bukkit.Bukkit; @@ -56,10 +50,8 @@ import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; -import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -70,63 +62,21 @@ import org.jetbrains.annotations.Nullable; */ public class OpenInv extends JavaPlugin implements IOpenInv { - private final Map inventories = new HashMap<>(); - private final Map enderChests = new HashMap<>(); - private final Multimap> pluginUsage = HashMultimap.create(); - - private final Cache playerCache = new Cache<>(300000L, - value -> { - String key = OpenInv.this.getPlayerID(value); - - return OpenInv.this.inventories.containsKey(key) - && OpenInv.this.inventories.get(key).isInUse() - || OpenInv.this.enderChests.containsKey(key) - && OpenInv.this.enderChests.get(key).isInUse() - || OpenInv.this.pluginUsage.containsKey(key); - }, - value -> { - String key = OpenInv.this.getPlayerID(value); - - // Check if inventory is stored, and if it is, remove it and eject all viewers - if (OpenInv.this.inventories.containsKey(key)) { - Inventory inv = OpenInv.this.inventories.remove(key).getBukkitInventory(); - List viewers = new ArrayList<>(inv.getViewers()); - for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) { - entity.closeInventory(); - } - } - - // Check if ender chest is stored, and if it is, remove it and eject all viewers - if (OpenInv.this.enderChests.containsKey(key)) { - Inventory inv = OpenInv.this.enderChests.remove(key).getBukkitInventory(); - List viewers = new ArrayList<>(inv.getViewers()); - for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) { - entity.closeInventory(); - } - } - - if (!OpenInv.this.disableSaving() && !value.isOnline()) { - value.saveData(); - } - }); + private final Map inventories = new ConcurrentHashMap<>(); + private final Map enderChests = new ConcurrentHashMap<>(); private InternalAccessor accessor; private LanguageManager languageManager; private boolean isSpigot = false; + private OfflineHandler offlineHandler; /** - * Evicts all viewers lacking cross-world permissions from a Player's inventory. + * Evict all viewers lacking cross-world permissions from a Player's inventory. * * @param player the Player */ - public void changeWorld(final Player player) { - - String key = this.getPlayerID(player); - - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { - return; - } + void changeWorld(@NotNull Player player) { + UUID key = player.getUniqueId(); if (this.inventories.containsKey(key)) { kickCrossWorldViewers(player, this.inventories.get(key)); @@ -137,15 +87,19 @@ public class OpenInv extends JavaPlugin implements IOpenInv { } } - private void kickCrossWorldViewers(Player player, ISpecialInventory inventory) { - List viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers()); - for (HumanEntity human : viewers) { - // If player has permission or is in the same world, allow continued access - // Just in case, also allow null worlds. - if (Permissions.CROSSWORLD.hasPermission(human) || Objects.equals(human.getWorld(), player.getWorld())) { - continue; + private void kickCrossWorldViewers(@NotNull Player player, @NotNull ISpecialInventory inventory) { + ejectViewers( + inventory, + viewer -> + !Permissions.CROSSWORLD.hasPermission(viewer) + && Objects.equals(viewer.getWorld(), player.getWorld())); + } + + static void ejectViewers(@NotNull ISpecialInventory inventory, Predicate predicate) { + for (HumanEntity viewer : new ArrayList<>(inventory.getBukkitInventory().getViewers())) { + if (predicate.test(viewer)) { + viewer.closeInventory(); } - human.closeInventory(); } } @@ -159,7 +113,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv { * @param rawSlot the raw slot in the view * @return the converted slot number */ - public int convertToPlayerSlot(InventoryView view, int rawSlot) { + int convertToPlayerSlot(InventoryView view, int rawSlot) { return this.accessor.getPlayerDataManager().convertToPlayerSlot(view, rawSlot); } @@ -168,9 +122,13 @@ public class OpenInv extends JavaPlugin implements IOpenInv { return this.getConfig().getBoolean("settings.disable-saving", false); } - @NotNull @Override - public IAnySilentContainer getAnySilentContainer() { + public boolean disableOfflineAccess() { + return this.getConfig().getBoolean("settings.disable-offline-access", false); + } + + @Override + public @NotNull IAnySilentContainer getAnySilentContainer() { return this.accessor.getAnySilentContainer(); } @@ -202,31 +160,31 @@ public class OpenInv extends JavaPlugin implements IOpenInv { return this.getConfig().getBoolean("toggles.silent-chest." + this.getPlayerID(offline), defaultState); } - @NotNull @Override - public ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) + public @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) throws InstantiationException { - String id = this.getPlayerID(player); - if (this.enderChests.containsKey(id)) { - return this.enderChests.get(id); + UUID key = player.getUniqueId(); + + if (this.enderChests.containsKey(key)) { + return this.enderChests.get(key); } + ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online); - this.enderChests.put(id, inv); - this.playerCache.put(id, player); + this.enderChests.put(key, inv); return inv; } - @NotNull @Override - public ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) + public @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) throws InstantiationException { - String id = this.getPlayerID(player); - if (this.inventories.containsKey(id)) { - return this.inventories.get(id); + UUID key = player.getUniqueId(); + + if (this.inventories.containsKey(key)) { + return this.inventories.get(key); } + ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online); - this.inventories.put(id, inv); - this.playerCache.put(id, player); + this.inventories.put(key, inv); return inv; } @@ -236,20 +194,28 @@ public class OpenInv extends JavaPlugin implements IOpenInv { } @Override - public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { + public boolean isPlayerLoaded(@NotNull UUID playerUuid) { + return this.inventories.containsKey(playerUuid) || this.enderChests.containsKey(playerUuid); + } - String key = this.getPlayerID(offline); - if (this.playerCache.containsKey(key)) { - return this.playerCache.get(key); + @Override + public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { + UUID key = offline.getUniqueId(); + + if (this.inventories.containsKey(key)) { + return (Player) this.inventories.get(key).getPlayer(); + } + + if (this.enderChests.containsKey(key)) { + return (Player) this.enderChests.get(key).getPlayer(); } Player player = offline.getPlayer(); if (player != null) { - this.playerCache.put(key, player); return player; } - if (!this.isSupportedVersion()) { + if (disableOfflineAccess() || !this.isSupportedVersion()) { return null; } @@ -267,10 +233,6 @@ public class OpenInv extends JavaPlugin implements IOpenInv { return null; } - if (player != null) { - this.playerCache.put(key, player); - } - return player; } @@ -338,9 +300,23 @@ public class OpenInv extends JavaPlugin implements IOpenInv { return; } - if (this.isSupportedVersion()) { - this.playerCache.invalidateAll(); - } + Stream.concat(inventories.values().stream(), enderChests.values().stream()) + .map(inventory -> { + // Cheat a bit - rather than stream twice, evict all viewers during remapping. + ejectViewers(inventory, viewer -> true); + if (inventory.getPlayer() instanceof Player player) { + return player; + } + return null; + }) + .filter(Objects::nonNull) + .distinct() + .forEach(player -> { + if (!player.isOnline()) { + player = accessor.getPlayerDataManager().inject(player); + } + player.saveData(); + }); } @Override @@ -355,6 +331,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv { this.accessor = new InternalAccessor(this); this.languageManager = new LanguageManager(this, "en_us"); + this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS; try { Class.forName("org.bukkit.entity.Player$Spigot"); @@ -371,7 +348,6 @@ public class OpenInv extends JavaPlugin implements IOpenInv { // Register listeners pm.registerEvents(new PlayerListener(this), this); - pm.registerEvents(new PluginListener(this), this); pm.registerEvents(new InventoryListener(this), this); // Register commands to their executors @@ -390,7 +366,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv { private void sendVersionError(Consumer messageMethod) { if (!this.accessor.isSupported()) { messageMethod.accept("Your server version (" + this.accessor.getVersion() + ") is not supported."); - messageMethod.accept("Please obtain an appropriate version here: " + this.accessor.getReleasesLink()); + messageMethod.accept("Please download the correct version of OpenInv here: " + this.accessor.getReleasesLink()); } if (!isSpigot) { messageMethod.accept("OpenInv requires that you use Spigot or a Spigot fork. Per the 1.14 update thread"); @@ -418,40 +394,10 @@ public class OpenInv extends JavaPlugin implements IOpenInv { return false; } - public void releaseAllPlayers(final Plugin plugin) { - Iterator>> iterator = this.pluginUsage.entries().iterator(); - - if (!iterator.hasNext()) { - return; - } - - for (Map.Entry> entry = iterator.next(); iterator.hasNext(); entry = iterator.next()) { - if (entry.getValue().equals(plugin.getClass())) { - iterator.remove(); - } - } - } - @Override - public void releasePlayer(@NotNull final Player player, @NotNull final Plugin plugin) { - String key = this.getPlayerID(player); - - if (!this.pluginUsage.containsEntry(key, plugin.getClass())) { - return; - } - - this.pluginUsage.remove(key, plugin.getClass()); - } - - @Override - public void retainPlayer(@NotNull final Player player, @NotNull final Plugin plugin) { - String key = this.getPlayerID(player); - - if (this.pluginUsage.containsEntry(key, plugin.getClass())) { - return; - } - - this.pluginUsage.put(key, plugin.getClass()); + public void reloadConfig() { + super.reloadConfig(); + this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS; } @Override @@ -464,27 +410,71 @@ public class OpenInv extends JavaPlugin implements IOpenInv { * Method for handling a Player going offline. * * @param player the Player - * @throws IllegalStateException if the server version is unsupported */ - public void setPlayerOffline(final Player player) { + void setPlayerOffline(@NotNull Player player) { + setPlayerOffline(player, offlineHandler); + } - String key = this.getPlayerID(player); + private void setPlayerOffline(@NotNull OfflinePlayer player, @NotNull OfflineHandler handler) { + UUID key = player.getUniqueId(); - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { + setPlayerOffline(inventories, key, handler); + setPlayerOffline(enderChests, key, handler); + } + + private void setPlayerOffline( + @NotNull Map map, + @NotNull UUID key, + @NotNull OfflineHandler handler) { + ISpecialInventory inventory = handler.fetch().apply(map, key); + if (inventory == null) { + return; + } + inventory.setPlayerOffline(); + if (!inventory.isInUse()) { + map.remove(key); + } else { + handler.handle().accept(inventory); + } + } + + void handleCloseInventory(@NotNull HumanEntity exViewer, @NotNull ISpecialInventory inventory) { + Map map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests; + UUID key = inventory.getPlayer().getUniqueId(); + @Nullable ISpecialInventory loaded = map.get(key); + + if (loaded == null) { + // Loaded inventory has already been removed. Removal will handle saving if necessary. return; } - // Replace stored player with our own version - this.playerCache.put(key, this.accessor.getPlayerDataManager().inject(player)); - - if (this.inventories.containsKey(key)) { - this.inventories.get(key).setPlayerOffline(); + if (loaded != inventory) { + Inventory bukkitInventory = inventory.getBukkitInventory(); + // Just in case, respect contents of the inventory that was just used. + loaded.getBukkitInventory().setContents(bukkitInventory.getContents()); + // We need to close this inventory to reduce risk of duplication bugs if the user is offline. + // We don't want to risk recursively closing the same inventory repeatedly, so we schedule dumping viewers. + // Worst case we schedule a couple redundant tasks if several people had the inventory open. + if (!bukkitInventory.getViewers().isEmpty()) { + getServer().getScheduler().runTask(this, () -> ejectViewers(inventory, viewer -> true)); + } } - if (this.enderChests.containsKey(key)) { - this.enderChests.get(key).setPlayerOffline(); - } + // Schedule task to check in use status later this tick. Closing user is still in viewer list. + getServer().getScheduler().runTask(this, () -> { + if (loaded.isInUse()) { + return; + } + + // Re-fetch from map - prevents duplicate saves on multi-close. + ISpecialInventory current = map.remove(key); + + if (!disableSaving() + && current != null + && current.getPlayer() instanceof Player player && !player.isOnline()) { + this.accessor.getPlayerDataManager().inject(player).saveData(); + } + }); } /** @@ -493,31 +483,34 @@ public class OpenInv extends JavaPlugin implements IOpenInv { * @param player the Player * @throws IllegalStateException if the server version is unsupported */ - public void setPlayerOnline(final Player player) { + void setPlayerOnline(@NotNull Player player) { + setPlayerOnline(inventories, player, player::updateInventory); + setPlayerOnline(enderChests, player, null); + } - String key = this.getPlayerID(player); + private void setPlayerOnline( + @NotNull Map map, + @NotNull Player player, + @Nullable Runnable task) { + ISpecialInventory inventory = map.get(player.getUniqueId()); - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { + if (inventory == null) { + // Inventory not open. return; } - this.playerCache.put(key, player); + inventory.setPlayerOnline(player); - if (this.inventories.containsKey(key)) { - this.inventories.get(key).setPlayerOnline(player); - new BukkitRunnable() { - @Override - public void run() { - if (player.isOnline()) { - player.updateInventory(); - } - } - }.runTask(this); - } + // Eject viewers lacking permission. + ejectViewers( + inventory, + viewer -> + !Permissions.OPENONLINE.hasPermission(viewer) + || !Permissions.CROSSWORLD.hasPermission(viewer) + && !Objects.equals(viewer.getWorld(), inventory.getPlayer().getWorld())); - if (this.enderChests.containsKey(key)) { - this.enderChests.get(key).setPlayerOnline(player); + if (task != null) { + getServer().getScheduler().runTask(this, task); } } @@ -529,7 +522,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv { @Override public void unload(@NotNull final OfflinePlayer offline) { - this.playerCache.invalidate(this.getPlayerID(offline)); + setPlayerOffline(offline, OfflineHandler.REMOVE_AND_CLOSE); } } diff --git a/plugin/src/main/java/com/lishid/openinv/PlayerListener.java b/plugin/src/main/java/com/lishid/openinv/PlayerListener.java index 4ec2950..61a5f7d 100644 --- a/plugin/src/main/java/com/lishid/openinv/PlayerListener.java +++ b/plugin/src/main/java/com/lishid/openinv/PlayerListener.java @@ -32,22 +32,22 @@ import org.jetbrains.annotations.NotNull; record PlayerListener(OpenInv plugin) implements Listener { @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerJoin(@NotNull PlayerJoinEvent event) { + private void onPlayerJoin(@NotNull PlayerJoinEvent event) { plugin.setPlayerOnline(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerQuit(@NotNull PlayerQuitEvent event) { + private void onPlayerQuit(@NotNull PlayerQuitEvent event) { plugin.setPlayerOffline(event.getPlayer()); } @EventHandler - public void onWorldChange(@NotNull PlayerChangedWorldEvent event) { + private void onWorldChange(@NotNull PlayerChangedWorldEvent event) { plugin.changeWorld(event.getPlayer()); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerInteract(@NotNull PlayerInteractEvent event) { + private void onPlayerInteract(@NotNull PlayerInteractEvent event) { // Do not cancel 3rd party plugins' custom events if (!PlayerInteractEvent.class.equals(event.getClass())) { diff --git a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java index c97b154..189a41d 100644 --- a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java +++ b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -133,7 +133,7 @@ public class OpenInvCommand implements TabExecutor { boolean online = target.isOnline(); if (!online) { - if (Permissions.OPENOFFLINE.hasPermission(player)) { + if (!plugin.disableOfflineAccess() && Permissions.OPENOFFLINE.hasPermission(player)) { // Try loading the player's data onlineTarget = this.plugin.loadPlayer(target); } else { diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java deleted file mode 100644 index 850c988..0000000 --- a/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2011-2021 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.listeners; - -import com.lishid.openinv.OpenInv; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.server.PluginDisableEvent; - -/** - * Listener for plugin-related events. - * - * @author Jikoo - */ -public class PluginListener implements Listener { - - private final OpenInv plugin; - - public PluginListener(OpenInv plugin) { - this.plugin = plugin; - } - - @EventHandler - public void onPluginDisable(PluginDisableEvent event) { - plugin.releaseAllPlayers(event.getPlugin()); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/Cache.java b/plugin/src/main/java/com/lishid/openinv/util/Cache.java deleted file mode 100644 index 5f2e8dc..0000000 --- a/plugin/src/main/java/com/lishid/openinv/util/Cache.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2011-2021 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Predicate; - -/** - * A minimal thread-safe time-based cache implementation backed by a HashMap and TreeMultimap. - * - * @author Jikoo - */ -public class Cache { - - private final Map internal; - private final Multimap expiry; - private final long retention; - private final Predicate inUseCheck; - private final Consumer postRemoval; - - /** - * Constructs a Cache with the specified retention duration, in use function, and post-removal function. - * - * @param retention duration after which keys are automatically invalidated if not in use - * @param inUseCheck Predicate used to check if a key is considered in use - * @param postRemoval Consumer used to perform any operations required when a key is invalidated - */ - public Cache(final long retention, final Predicate inUseCheck, final Consumer postRemoval) { - this.internal = new HashMap<>(); - - this.expiry = TreeMultimap.create(Long::compareTo, (k1, k2) -> Objects.equals(k1, k2) ? 0 : 1); - - this.retention = retention; - this.inUseCheck = inUseCheck; - this.postRemoval = postRemoval; - } - - /** - * Set a key and value pair. Keys are unique. Using an existing key will cause the old value to - * be overwritten and the expiration timer to be reset. - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - */ - public void put(final K key, final V value) { - // Invalidate key - runs lazy check and ensures value won't be cleaned up early - this.invalidate(key); - - synchronized (this.internal) { - this.internal.put(key, value); - this.expiry.put(System.currentTimeMillis() + this.retention, key); - } - } - - /** - * Returns the value to which the specified key is mapped, or null if no value is mapped for the key. - * - * @param key the key whose associated value is to be returned - * @return the value to which the specified key is mapped, or null if no value is mapped for the key - */ - public V get(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - return this.internal.get(key); - } - } - - /** - * Returns true if the specified key is mapped to a value. - * - * @param key key to check if a mapping exists for - * @return true if a mapping exists for the specified key - */ - public boolean containsKey(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - return this.internal.containsKey(key); - } - } - - /** - * Forcibly invalidates a key, even if it is considered to be in use. - * - * @param key key to invalidate - */ - public void invalidate(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - if (!this.internal.containsKey(key)) { - // Value either not present or cleaned by lazy check. Either way, we're good - return; - } - - // Remove stored object - this.internal.remove(key); - - // Remove expiration entry - prevents more work later, plus prevents issues with values invalidating early - for (Iterator> iterator = this.expiry.entries().iterator(); iterator.hasNext();) { - if (key.equals(iterator.next().getValue())) { - iterator.remove(); - break; - } - } - } - } - - /** - * Forcibly invalidates all keys, even if they are considered to be in use. - */ - public void invalidateAll() { - synchronized (this.internal) { - for (V value : this.internal.values()) { - this.postRemoval.accept(value); - } - this.expiry.clear(); - this.internal.clear(); - } - } - - /** - * Invalidate all expired keys that are not considered in use. If a key is expired but is - * considered in use by the provided Function, its expiration time is reset. - */ - private void lazyCheck() { - long now = System.currentTimeMillis(); - synchronized (this.internal) { - List inUse = new ArrayList<>(); - for (Iterator> iterator = this.expiry.entries().iterator(); iterator - .hasNext();) { - Map.Entry entry = iterator.next(); - - if (entry.getKey() > now) { - break; - } - - iterator.remove(); - - if (this.inUseCheck.test(this.internal.get(entry.getValue()))) { - inUse.add(entry.getValue()); - continue; - } - - V value = this.internal.remove(entry.getValue()); - - if (value == null) { - continue; - } - - this.postRemoval.accept(value); - } - - long nextExpiry = now + this.retention; - for (K value : inUse) { - this.expiry.put(nextExpiry, value); - } - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java b/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java index 9ed6fb8..6883999 100644 --- a/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java +++ b/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,13 +25,7 @@ import java.util.Set; import org.bukkit.OfflinePlayer; import org.bukkit.configuration.ConfigurationSection; -public class ConfigUpdater { - - private final OpenInv plugin; - - public ConfigUpdater(OpenInv plugin) { - this.plugin = plugin; - } +public record ConfigUpdater(OpenInv plugin) { public void checkForUpdates() { final int version = plugin.getConfig().getInt("config-version", 1); @@ -60,6 +54,9 @@ public class ConfigUpdater { if (version < 4) { updateConfig3To4(); } + if (version < 5) { + updateConfig4To5(); + } plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.saveConfig(); @@ -68,6 +65,13 @@ public class ConfigUpdater { }); } + private void updateConfig4To5() { + plugin.getServer().getScheduler().runTask(plugin, () -> { + plugin.getConfig().set("settings.disable-offline-access", false); + plugin.getConfig().set("config-version", 5); + }); + } + private void updateConfig3To4() { plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.getConfig().set("notify", null); diff --git a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java b/plugin/src/main/java/com/lishid/openinv/util/Permissions.java index 7876af3..c2af2e1 100644 --- a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java +++ b/plugin/src/main/java/com/lishid/openinv/util/Permissions.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2021 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ package com.lishid.openinv.util; import org.bukkit.permissions.Permissible; +import org.jetbrains.annotations.NotNull; public enum Permissions { @@ -50,7 +51,7 @@ public enum Permissions { this.uninheritable = uninheritable; } - public boolean hasPermission(Permissible permissible) { + public boolean hasPermission(@NotNull Permissible permissible) { boolean hasPermission = permissible.hasPermission(permission); if (uninheritable || hasPermission || permissible.isPermissionSet(permission)) { diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml index d8bc7bb..f0b56f0 100644 --- a/plugin/src/main/resources/config.yml +++ b/plugin/src/main/resources/config.yml @@ -1,4 +1,5 @@ -config-version: 4 +config-version: 5 settings: + disable-offline-access: false disable-saving: false locale: 'en_us'