[Idea]: Folia support for OpenInv #196

Closed
reabuc wants to merge 137 commits from master into master
68 changed files with 6397 additions and 2459 deletions
Showing only changes of commit fdf920062b - Show all commits

View File

@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <pre>
* public &#64;NotNull Player savePlayerData(&#64;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;
* }
* </pre>
*/
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.

View File

@@ -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;
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Map<UUID, ? extends ISpecialInventory>, 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))
);
}

View File

@@ -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<String, ISpecialPlayerInventory> inventories = new HashMap<>();
private final Map<String, ISpecialEnderChest> enderChests = new HashMap<>();
private final Multimap<String, Class<? extends Plugin>> pluginUsage = HashMultimap.create();
private final Cache<String, Player> 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<HumanEntity> 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<HumanEntity> 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<UUID, ISpecialPlayerInventory> inventories = new ConcurrentHashMap<>();
private final Map<UUID, ISpecialEnderChest> 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<HumanEntity> 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<HumanEntity> 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<String> 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<Map.Entry<String, Class<? extends Plugin>>> iterator = this.pluginUsage.entries().iterator();
if (!iterator.hasNext()) {
return;
}
for (Map.Entry<String, Class<? extends Plugin>> 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<UUID, ? extends ISpecialInventory> 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<UUID, ? extends ISpecialInventory> 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<UUID, ? extends ISpecialInventory> 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);
}
}

View File

@@ -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())) {

View File

@@ -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 {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<K, V> {
private final Map<K, V> internal;
private final Multimap<Long, K> expiry;
private final long retention;
private final Predicate<V> inUseCheck;
private final Consumer<V> 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<V> inUseCheck, final Consumer<V> 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<Map.Entry<Long, K>> 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<K> inUse = new ArrayList<>();
for (Iterator<Map.Entry<Long, K>> iterator = this.expiry.entries().iterator(); iterator
.hasNext();) {
Map.Entry<Long, K> 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);
}
}
}
}

View File

@@ -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);

View File

@@ -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)) {

View File

@@ -1,4 +1,5 @@
config-version: 4
config-version: 5
settings:
disable-offline-access: false
disable-saving: false
locale: 'en_us'