[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 * 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 * 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 { public interface IOpenInv {
/** /**
* Check the configuration value for whether or not OpenInv saves player data when unloading * Check the configuration value for whether OpenInv saves player data when unloading players. This is exclusively
* players. This is exclusively for users who do not allow editing of inventories, only viewing, * for users who do not allow editing of inventories, only viewing, and wish to prevent any possibility of bugs such
* and wish to prevent any possibility of bugs such as lishid#40. If true, OpenInv will not ever * as lishid#40. If true, OpenInv will not ever save any edits made to players.
* save any edits made to players.
* *
* @return false unless configured otherwise * @return false unless configured otherwise
*/ */
boolean disableSaving(); 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. * Gets the active ISilentContainer implementation.
* *
@@ -60,12 +68,9 @@ public interface IOpenInv {
@NotNull IAnySilentContainer getAnySilentContainer(); @NotNull IAnySilentContainer getAnySilentContainer();
/** /**
* Gets the active IInventoryAccess implementation. * @deprecated Use static {@link InventoryAccess} methods.
*
* @return the IInventoryAccess
* @throws IllegalStateException if the server version is unsupported
*/ */
@Deprecated @Deprecated(forRemoval = true)
default @NotNull IInventoryAccess getInventoryAccess() { default @NotNull IInventoryAccess getInventoryAccess() {
return new InventoryAccess(); return new InventoryAccess();
} }
@@ -129,6 +134,15 @@ public interface IOpenInv {
*/ */
boolean isSupportedVersion(); 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. * Load a Player from an OfflinePlayer. May return null under some circumstances.
* *
@@ -227,57 +241,51 @@ public interface IOpenInv {
* @return true unless configured otherwise * @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 or not they show is based on language settings.
*/ */
@Deprecated @Deprecated(forRemoval = true)
default boolean notifyAnyChest() { default boolean notifyAnyChest() {
return true; return true;
} }
/** /**
* Check the configuration value for whether or not OpenInv displays a notification to the user * @deprecated OpenInv uses action bar chat for notifications. Whether they show is based on language settings.
* 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 @Deprecated(forRemoval = true)
default boolean notifySilentChest() { default boolean notifySilentChest() {
return true; return true;
} }
/** /**
* Mark a Player as no longer in use by a Plugin to allow OpenInv to remove it from the cache * @deprecated see {@link #retainPlayer(Player, Plugin)}
* 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
*/ */
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 * @deprecated OpenInv no longer uses an internal cache beyond maintaining copies of currently open inventories.
* prevent issues with multiple copies of the same Player being loaded such as lishid#49. * If you wish to use/modify a player, ensure either {@link IOpenInv#isPlayerLoaded(UUID)} is false or the player
* Changes made to loaded copies overwrite changes to the others when saved, leading to * instance is the same memory address as the one in use by OpenInv.
* duplication bugs and more. * <pre>
* <p> * public &#64;NotNull Player savePlayerData(&#64;NotNull Player player) {
* When finished with the Player object, be sure to call {@link #releasePlayer(Player, Plugin)} * IOpenInv openInv = ...
* to prevent the cache from keeping it stored until the plugin is disabled. * if (!openInv.disableSaving() && openInv.isPlayerLoaded(player.getUniqueId())) {
* <p> * Player openInvLoadedPlayer = openInv.loadPlayer(myInUsePlayer);
* When using a Player object from OpenInv, you must handle the Player coming online, replacing * if (openInvLoadedPlayer != player) {
* your Player reference with the Player from the PlayerJoinEvent. In addition, you must change * // The copy loaded by OpenInv is not the same as our loaded copy. Push our changes.
* any values in the Player to reflect any unsaved alterations to the existing Player which do * copyPlayerModifications(player, openInvLoadedPlayer);
* not affect the inventory or ender chest contents. * }
* <p> * // OpenInv will handle saving data when the player is unloaded.
* OpenInv only saves player data when unloading a Player from the cache, and then only if * // Optionally, to be sure our changes will persist, save now.
* {@link #disableSaving()} returns false. If you are making changes that OpenInv does not cause * // openInvLoadedPlayer.saveData();
* to persist when a Player logs in as noted above, it is suggested that you manually call * return openInvLoadedPlayer;
* {@link Player#saveData()} when releasing your reference to ensure your changes persist. * }
* *
* @param player the Player * player.saveData();
* @param plugin the Plugin holding the reference to the Player * return player;
* @throws IllegalStateException if the server version is unsupported * }
* </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. * Sets a player's AnyChest setting.

View File

@@ -16,6 +16,7 @@
package com.lishid.openinv; package com.lishid.openinv;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.util.InventoryAccess; import com.lishid.openinv.util.InventoryAccess;
import com.lishid.openinv.util.Permissions; import com.lishid.openinv.util.Permissions;
@@ -47,7 +48,7 @@ import org.jetbrains.annotations.Nullable;
record InventoryListener(OpenInv plugin) implements Listener { record InventoryListener(OpenInv plugin) implements Listener {
@EventHandler @EventHandler
public void onInventoryClose(@NotNull final InventoryCloseEvent event) { private void onInventoryClose(@NotNull final InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) { if (!(event.getPlayer() instanceof Player player)) {
return; return;
} }
@@ -55,10 +56,20 @@ record InventoryListener(OpenInv plugin) implements Listener {
if (this.plugin.getPlayerSilentChestStatus(player)) { if (this.plugin.getPlayerSilentChestStatus(player)) {
this.plugin.getAnySilentContainer().deactivateContainer(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) @EventHandler(priority = EventPriority.LOWEST)
public void onInventoryClick(@NotNull final InventoryClickEvent event) { private void onInventoryClick(@NotNull final InventoryClickEvent event) {
if (handleInventoryInteract(event)) { if (handleInventoryInteract(event)) {
return; return;
} }
@@ -92,7 +103,7 @@ record InventoryListener(OpenInv plugin) implements Listener {
} }
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
public void onInventoryDrag(@NotNull final InventoryDragEvent event) { private void onInventoryDrag(@NotNull final InventoryDragEvent event) {
if (handleInventoryInteract(event)) { if (handleInventoryInteract(event)) {
return; 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 * 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 * it under the terms of the GNU General Public License as published by
@@ -16,8 +16,6 @@
package com.lishid.openinv; 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.ContainerSettingCommand;
import com.lishid.openinv.commands.OpenInvCommand; import com.lishid.openinv.commands.OpenInvCommand;
import com.lishid.openinv.commands.SearchContainerCommand; 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.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory; 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.ConfigUpdater;
import com.lishid.openinv.util.InternalAccessor;
import com.lishid.openinv.util.LanguageManager; import com.lishid.openinv.util.LanguageManager;
import com.lishid.openinv.util.Permissions; import com.lishid.openinv.util.Permissions;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.function.Consumer; 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.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -56,10 +50,8 @@ import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory; import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.InventoryView;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -70,63 +62,21 @@ import org.jetbrains.annotations.Nullable;
*/ */
public class OpenInv extends JavaPlugin implements IOpenInv { public class OpenInv extends JavaPlugin implements IOpenInv {
private final Map<String, ISpecialPlayerInventory> inventories = new HashMap<>(); private final Map<UUID, ISpecialPlayerInventory> inventories = new ConcurrentHashMap<>();
private final Map<String, ISpecialEnderChest> enderChests = new HashMap<>(); private final Map<UUID, ISpecialEnderChest> enderChests = new ConcurrentHashMap<>();
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 InternalAccessor accessor; private InternalAccessor accessor;
private LanguageManager languageManager; private LanguageManager languageManager;
private boolean isSpigot = false; 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 * @param player the Player
*/ */
public void changeWorld(final Player player) { void changeWorld(@NotNull Player player) {
UUID key = player.getUniqueId();
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;
}
if (this.inventories.containsKey(key)) { if (this.inventories.containsKey(key)) {
kickCrossWorldViewers(player, this.inventories.get(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) { private void kickCrossWorldViewers(@NotNull Player player, @NotNull ISpecialInventory inventory) {
List<HumanEntity> viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers()); ejectViewers(
for (HumanEntity human : viewers) { inventory,
// If player has permission or is in the same world, allow continued access viewer ->
// Just in case, also allow null worlds. !Permissions.CROSSWORLD.hasPermission(viewer)
if (Permissions.CROSSWORLD.hasPermission(human) || Objects.equals(human.getWorld(), player.getWorld())) { && Objects.equals(viewer.getWorld(), player.getWorld()));
continue; }
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 * @param rawSlot the raw slot in the view
* @return the converted slot number * @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); 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); return this.getConfig().getBoolean("settings.disable-saving", false);
} }
@NotNull
@Override @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(); 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); return this.getConfig().getBoolean("toggles.silent-chest." + this.getPlayerID(offline), defaultState);
} }
@NotNull
@Override @Override
public ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) public @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online)
throws InstantiationException { throws InstantiationException {
String id = this.getPlayerID(player); UUID key = player.getUniqueId();
if (this.enderChests.containsKey(id)) {
return this.enderChests.get(id); if (this.enderChests.containsKey(key)) {
return this.enderChests.get(key);
} }
ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online); ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online);
this.enderChests.put(id, inv); this.enderChests.put(key, inv);
this.playerCache.put(id, player);
return inv; return inv;
} }
@NotNull
@Override @Override
public ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) public @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online)
throws InstantiationException { throws InstantiationException {
String id = this.getPlayerID(player); UUID key = player.getUniqueId();
if (this.inventories.containsKey(id)) {
return this.inventories.get(id); if (this.inventories.containsKey(key)) {
return this.inventories.get(key);
} }
ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online); ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online);
this.inventories.put(id, inv); this.inventories.put(key, inv);
this.playerCache.put(id, player);
return inv; return inv;
} }
@@ -236,20 +194,28 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
} }
@Override @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); @Override
if (this.playerCache.containsKey(key)) { public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
return this.playerCache.get(key); 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(); Player player = offline.getPlayer();
if (player != null) { if (player != null) {
this.playerCache.put(key, player);
return player; return player;
} }
if (!this.isSupportedVersion()) { if (disableOfflineAccess() || !this.isSupportedVersion()) {
return null; return null;
} }
@@ -267,10 +233,6 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
return null; return null;
} }
if (player != null) {
this.playerCache.put(key, player);
}
return player; return player;
} }
@@ -338,9 +300,23 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
return; return;
} }
if (this.isSupportedVersion()) { Stream.concat(inventories.values().stream(), enderChests.values().stream())
this.playerCache.invalidateAll(); .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 @Override
@@ -355,6 +331,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
this.accessor = new InternalAccessor(this); this.accessor = new InternalAccessor(this);
this.languageManager = new LanguageManager(this, "en_us"); this.languageManager = new LanguageManager(this, "en_us");
this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS;
try { try {
Class.forName("org.bukkit.entity.Player$Spigot"); Class.forName("org.bukkit.entity.Player$Spigot");
@@ -371,7 +348,6 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
// Register listeners // Register listeners
pm.registerEvents(new PlayerListener(this), this); pm.registerEvents(new PlayerListener(this), this);
pm.registerEvents(new PluginListener(this), this);
pm.registerEvents(new InventoryListener(this), this); pm.registerEvents(new InventoryListener(this), this);
// Register commands to their executors // Register commands to their executors
@@ -390,7 +366,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
private void sendVersionError(Consumer<String> messageMethod) { private void sendVersionError(Consumer<String> messageMethod) {
if (!this.accessor.isSupported()) { if (!this.accessor.isSupported()) {
messageMethod.accept("Your server version (" + this.accessor.getVersion() + ") is not supported."); 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) { if (!isSpigot) {
messageMethod.accept("OpenInv requires that you use Spigot or a Spigot fork. Per the 1.14 update thread"); 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; 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 @Override
public void releasePlayer(@NotNull final Player player, @NotNull final Plugin plugin) { public void reloadConfig() {
String key = this.getPlayerID(player); super.reloadConfig();
this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS;
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());
} }
@Override @Override
@@ -464,27 +410,71 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
* Method for handling a Player going offline. * Method for handling a Player going offline.
* *
* @param player the Player * @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. setPlayerOffline(inventories, key, handler);
if (!this.playerCache.containsKey(key)) { 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; return;
} }
// Replace stored player with our own version if (loaded != inventory) {
this.playerCache.put(key, this.accessor.getPlayerDataManager().inject(player)); Inventory bukkitInventory = inventory.getBukkitInventory();
// Just in case, respect contents of the inventory that was just used.
if (this.inventories.containsKey(key)) { loaded.getBukkitInventory().setContents(bukkitInventory.getContents());
this.inventories.get(key).setPlayerOffline(); // 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)) { // Schedule task to check in use status later this tick. Closing user is still in viewer list.
this.enderChests.get(key).setPlayerOffline(); 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 * @param player the Player
* @throws IllegalStateException if the server version is unsupported * @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 (inventory == null) {
if (!this.playerCache.containsKey(key)) { // Inventory not open.
return; return;
} }
this.playerCache.put(key, player); inventory.setPlayerOnline(player);
if (this.inventories.containsKey(key)) { // Eject viewers lacking permission.
this.inventories.get(key).setPlayerOnline(player); ejectViewers(
new BukkitRunnable() { inventory,
@Override viewer ->
public void run() { !Permissions.OPENONLINE.hasPermission(viewer)
if (player.isOnline()) { || !Permissions.CROSSWORLD.hasPermission(viewer)
player.updateInventory(); && !Objects.equals(viewer.getWorld(), inventory.getPlayer().getWorld()));
}
}
}.runTask(this);
}
if (this.enderChests.containsKey(key)) { if (task != null) {
this.enderChests.get(key).setPlayerOnline(player); getServer().getScheduler().runTask(this, task);
} }
} }
@@ -529,7 +522,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
@Override @Override
public void unload(@NotNull final OfflinePlayer offline) { 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 { record PlayerListener(OpenInv plugin) implements Listener {
@EventHandler(priority = EventPriority.LOWEST) @EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(@NotNull PlayerJoinEvent event) { private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
plugin.setPlayerOnline(event.getPlayer()); plugin.setPlayerOnline(event.getPlayer());
} }
@EventHandler(priority = EventPriority.MONITOR) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(@NotNull PlayerQuitEvent event) { private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
plugin.setPlayerOffline(event.getPlayer()); plugin.setPlayerOffline(event.getPlayer());
} }
@EventHandler @EventHandler
public void onWorldChange(@NotNull PlayerChangedWorldEvent event) { private void onWorldChange(@NotNull PlayerChangedWorldEvent event) {
plugin.changeWorld(event.getPlayer()); plugin.changeWorld(event.getPlayer());
} }
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) @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 // Do not cancel 3rd party plugins' custom events
if (!PlayerInteractEvent.class.equals(event.getClass())) { 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 * 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 * 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(); boolean online = target.isOnline();
if (!online) { if (!online) {
if (Permissions.OPENOFFLINE.hasPermission(player)) { if (!plugin.disableOfflineAccess() && Permissions.OPENOFFLINE.hasPermission(player)) {
// Try loading the player's data // Try loading the player's data
onlineTarget = this.plugin.loadPlayer(target); onlineTarget = this.plugin.loadPlayer(target);
} else { } 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 * 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 * 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.OfflinePlayer;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
public class ConfigUpdater { public record ConfigUpdater(OpenInv plugin) {
private final OpenInv plugin;
public ConfigUpdater(OpenInv plugin) {
this.plugin = plugin;
}
public void checkForUpdates() { public void checkForUpdates() {
final int version = plugin.getConfig().getInt("config-version", 1); final int version = plugin.getConfig().getInt("config-version", 1);
@@ -60,6 +54,9 @@ public class ConfigUpdater {
if (version < 4) { if (version < 4) {
updateConfig3To4(); updateConfig3To4();
} }
if (version < 5) {
updateConfig4To5();
}
plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.saveConfig(); 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() { private void updateConfig3To4() {
plugin.getServer().getScheduler().runTask(plugin, () -> { plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("notify", null); 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 * 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 * it under the terms of the GNU General Public License as published by
@@ -17,6 +17,7 @@
package com.lishid.openinv.util; package com.lishid.openinv.util;
import org.bukkit.permissions.Permissible; import org.bukkit.permissions.Permissible;
import org.jetbrains.annotations.NotNull;
public enum Permissions { public enum Permissions {
@@ -50,7 +51,7 @@ public enum Permissions {
this.uninheritable = uninheritable; this.uninheritable = uninheritable;
} }
public boolean hasPermission(Permissible permissible) { public boolean hasPermission(@NotNull Permissible permissible) {
boolean hasPermission = permissible.hasPermission(permission); boolean hasPermission = permissible.hasPermission(permission);
if (uninheritable || hasPermission || permissible.isPermissionSet(permission)) { if (uninheritable || hasPermission || permissible.isPermissionSet(permission)) {

View File

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