Open a player's inventory. If unspecified, will select last player opened or own if none opened previously.
-
-
-
/openender [player]
-
oe
-
Open a player's ender chest. If unspecified, will select last player opened or own if none opened previously.
-
-
-
/searchinv <item> [minAmount]
-
si
-
Lists all online players that have a certain item in their inventory.
-
-
-
/searchender <item> [minAmount]
-
se
-
Lists all online players that have a certain item in their ender chest.
-
-
-
/searchenchant <[enchantment] [MinLevel]>
-
searchenchants
-
Lists all online players with a specific enchantment.
-
-
-
/anycontainer [check]
-
ac, anychest
-
Check or toggle the AnyContainer function, allowing opening blocked containers.
-
-
-
/silentcontainer [check]
-
sc, silentchest
-
Check or toggle the SilentContainer function, allowing opening containers silently.
-
-
-
-## Permissions
-
-
-
Node
-
Description
-
-
-
OpenInv.*
-
Gives permission to use all of OpenInv.
-
-
-
OpenInv.openinv
-
Required to use /openinv.
-
-
-
OpenInv.openself
-
Required to open own inventory.
-
-
-
OpenInv.editinv
-
Required to make changes to open inventories.
-
-
-
OpenInv.openonline
-
Allows users to open online players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.
-
-
-
OpenInv.openoffline
-
Allows users to open offline players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.
-
-
-
OpenInv.openender
-
Required to use /openender.
-
-
-
OpenInv.editender
-
Required to make changes to open ender chests.
-
-
-
OpenInv.openenderall
-
Allows users to open others' ender chests. Without it, users can only open their own.
-
-
-
OpenInv.exempt
-
Prevents the player's inventory being opened by others.
-
-
-
OpenInv.override
-
Allows bypassing of the exempt permission.
-
-
-
OpenInv.crossworld
-
Allows cross-world usage of /openinv and /openender.
-
-
-
OpenInv.search
-
Required to use /searchinv and /searchender.
-
-
-
OpenInv.searchenchant
-
Required to use /searchenchant.
-
-
-
OpenInv.anychest
-
Required to use /anychest.
-
-
-
OpenInv.any.default
-
Cause AnyContainer to be enabled by default.
-
-
-
OpenInv.silent
-
Required to use /silentcontainer.
-
-
-
OpenInv.silent.default
-
Cause SilentContainer to be enabled by default.
-
-
-
OpenInv.spectate
-
Allows users in spectate gamemode to edit inventories.
-
-
-
-## For Developers
-To compile, the relevant Craftbukkit/Spigot jars must be installed in your local repository using the install plugin.
-Ex: `mvn install:install-file -Dpackaging=jar -Dfile=spigot-1.8-R0.1-SNAPSHOT.jar -DgroupId=org.spigotmc -DartifactId=spigot -Dversion=1.8-R0.1-SNAPSHOT`
-
-To compile for a single version, specify the NMS revision you are targeting: `mvn -pl -am clean install`
-
-To compile for a set of versions, you'll need to use a profile. The only provided profile is `all`. Select a profile using the `-P` argument: `mvn clean package -am -P all`
-
-For more information, check out the [official Maven guide](http://maven.apache.org/guides/introduction/introduction-to-profiles.html).
-
-The final file is `target/OpenInv.jar`
-
-## License
-```
-Copyright (C) 2011-2020 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 .
-```
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..618e7860
--- /dev/null
+++ b/README.md
@@ -0,0 +1,62 @@
+## About
+
+OpenInv is a [Bukkit plugin](https://dev.bukkit.org/projects/openinv) which allows users to open and edit anyone's
+inventory or ender chest - online or not!
+
+## Features
+
+- **OpenInv**: Open anyone's inventory, even if they're offline.
+ - Read-only mode! Don't grant edit permission.
+ - Cross-world support! Allow access only from the same world.
+ - No duplicate slots! Only armor is accessible when opening self (if allowed at all)!
+ - Drop items as the player! Place items in the dropper slot in the bottom right. Can be disabled via permission!
+ - Allow any item in armor slots! Configurable via permission.
+- **OpenEnder**: Open anyone's ender chest, even if they're offline.
+ - Allow access only to own ender chest! Don't grant permission to open others.
+ - Read-only mode! Don't grant edit permission.
+ - Cross-world support! Allow access only from the same world.
+- **SilentContainer**: Open containers without displaying an animation or making sound.
+- **AnyContainer**: Open containers, even if blocked by ocelots or blocks.
+
+## Commands
+
+See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Commands).
+
+## Permissions
+
+See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Permissions)
+
+## For Developers
+
+### As a Dependency
+
+The OpenInv API is available via [JitPack](https://jitpack.io/).
+
+```xml
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+```
+
+```xml
+
+
+ com.github.Jikoo
+ OpenInv
+ ${openinv.version}
+
+
+```
+
+Note that since JitPack only builds the API now, the "full" OpenInv jar on JitPack is actually the openinvapi artifact.
+This is a change from previous dependency declaration that I hope to revert.
+
+### Compilation
+
+Execute the gradle wrapper:
+`./gradlew build`
+
+If you encounter issues with building the Spigot module, try running BuildTools manually.
diff --git a/addon/togglepersist/build.gradle.kts b/addon/togglepersist/build.gradle.kts
new file mode 100644
index 00000000..78cb9095
--- /dev/null
+++ b/addon/togglepersist/build.gradle.kts
@@ -0,0 +1,21 @@
+plugins {
+ `openinv-base`
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+}
+
+tasks.processResources {
+ expand("version" to version)
+}
+
+tasks.register("distributeAddons") {
+ into(rootProject.layout.projectDirectory.dir("dist"))
+ from(tasks.jar)
+ rename("openinvtogglepersist.*\\.jar", "OITogglePersist.jar")
+}
+
+tasks.assemble {
+ dependsOn(tasks.named("distributeAddons"))
+}
diff --git a/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java b/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java
new file mode 100644
index 00000000..6a0cc298
--- /dev/null
+++ b/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java
@@ -0,0 +1,152 @@
+package com.github.jikoo.openinv.togglepersist;
+
+import com.google.errorprone.annotations.Keep;
+import com.lishid.openinv.event.PlayerToggledEvent;
+import com.lishid.openinv.util.setting.PlayerToggle;
+import com.lishid.openinv.util.setting.PlayerToggles;
+import org.bukkit.configuration.Configuration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+
+public class TogglePersist extends JavaPlugin implements Listener {
+
+ private final Map> enabledToggles = new HashMap<>();
+
+ @Override
+ public void onEnable() {
+ getServer().getPluginManager().registerEvents(this, this);
+
+ File file = new File(getDataFolder(), "toggles.yml");
+
+ // If there's no save file, there's nothing to load.
+ if (!file.exists()) {
+ return;
+ }
+
+ Configuration loaded = YamlConfiguration.loadConfiguration(file);
+
+ // For each toggle, enable loaded players.
+ for (String toggleName : loaded.getKeys(false)) {
+ PlayerToggle toggle = PlayerToggles.get(toggleName);
+ // Ensure toggle exists.
+ if (toggle == null) {
+ continue;
+ }
+
+ for (String idString : loaded.getStringList(toggleName)) {
+ // Ensure valid UUID.
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(idString);
+ } catch (IllegalArgumentException e) {
+ continue;
+ }
+
+ // Track that toggle is enabled.
+ set(uuid, toggleName);
+ }
+ }
+ }
+
+ private void set(UUID playerId, String toggleName) {
+ enabledToggles.compute(
+ playerId,
+ (uuid, toggles) -> {
+ if (toggles == null) {
+ toggles = new HashSet<>();
+ }
+ toggles.add(toggleName);
+ return toggles;
+ }
+ );
+ }
+
+ @Override
+ public void onDisable() {
+ Map> converted = getSaveData();
+
+ YamlConfiguration data = new YamlConfiguration();
+ for (Map.Entry> playerToggle : converted.entrySet()) {
+ data.set(playerToggle.getKey(), playerToggle.getValue());
+ }
+
+ File file = new File(getDataFolder(), "toggles.yml");
+ try {
+ data.save(file);
+ } catch (IOException e) {
+ getLogger().log(Level.SEVERE, "Unable to save player toggle states", e);
+ }
+ }
+
+ private @NotNull Map> getSaveData() {
+ Map> converted = new HashMap<>();
+
+ for (Map.Entry> playerToggles : enabledToggles.entrySet()) {
+ String idString = playerToggles.getKey().toString();
+ for (String toggleName : playerToggles.getValue()) {
+ // Add player ID to listing for each enabled toggle.
+ converted.compute(
+ toggleName,
+ (name, ids) -> {
+ if (ids == null) {
+ ids = new ArrayList<>();
+ }
+ ids.add(idString);
+ return ids;
+ }
+ );
+ }
+ }
+ return converted;
+ }
+
+ @Keep
+ @EventHandler
+ private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
+ UUID playerId = event.getPlayer().getUniqueId();
+ Set toggleNames = enabledToggles.get(playerId);
+
+ if (toggleNames == null) {
+ return;
+ }
+
+ for (String toggleName : toggleNames) {
+ PlayerToggle toggle = PlayerToggles.get(toggleName);
+ if (toggle != null) {
+ toggle.set(playerId, true);
+ }
+ }
+ }
+
+ @Keep
+ @EventHandler
+ private void onToggleSet(@NotNull PlayerToggledEvent event) {
+ if (event.isEnabled()) {
+ set(event.getPlayerId(), event.getToggle().getName());
+ } else {
+ enabledToggles.computeIfPresent(
+ event.getPlayerId(),
+ (uuid, toggles) -> {
+ toggles.remove(event.getToggle().getName());
+ return toggles.isEmpty() ? null : toggles;
+ }
+ );
+ }
+ }
+
+}
diff --git a/addon/togglepersist/src/main/resources/plugin.yml b/addon/togglepersist/src/main/resources/plugin.yml
new file mode 100644
index 00000000..3760e30f
--- /dev/null
+++ b/addon/togglepersist/src/main/resources/plugin.yml
@@ -0,0 +1,7 @@
+name: OITogglePersist
+main: com.github.jikoo.openinv.togglepersist.TogglePersist
+version: ${version}
+author: Jikoo
+description: An OpenInv addon allowing /anycontainer and /silentcontainer to persist across sessions.
+api-version: "1.20"
+depend: [ OpenInv ]
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
new file mode 100644
index 00000000..9b2665ad
--- /dev/null
+++ b/api/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ `openinv-base`
+ `maven-publish`
+}
+
+publishing {
+ publications {
+ create("jitpack") {
+ groupId = "com.github.Jikoo.OpenInv"
+ artifactId = "openinvapi"
+ from(components["java"])
+ }
+ }
+}
diff --git a/api/pom.xml b/api/pom.xml
deleted file mode 100644
index cc34012f..00000000
--- a/api/pom.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-
-
-
- 4.0.0
-
-
- com.lishid
- openinvparent
- 4.1.6-SNAPSHOT
-
-
- openinvapi
- OpenInvAPI
-
-
-
- org.jetbrains
- annotations
- 17.0.0
-
-
- org.spigotmc
- spigot-api
- 1.16.5-R0.1-SNAPSHOT
- provided
-
-
-
-
-
-
- maven-compiler-plugin
- 3.8.1
-
- 1.8
- 1.8
-
-
-
-
-
-
diff --git a/api/src/main/java/com/lishid/openinv/IOpenInv.java b/api/src/main/java/com/lishid/openinv/IOpenInv.java
index b4f4e8f5..9634f7af 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-2020 lishid. All rights reserved.
+ * Copyright (C) 2011-2023 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,293 +17,182 @@
package com.lishid.openinv;
import com.lishid.openinv.internal.IAnySilentContainer;
-import com.lishid.openinv.internal.IInventoryAccess;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
-import com.lishid.openinv.util.InventoryAccess;
-import com.lishid.openinv.util.StringMetric;
-import java.util.UUID;
-import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
-import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.UUID;
+import java.util.logging.Logger;
+
/**
* Interface defining behavior for the OpenInv plugin.
- *
- * @author Jikoo
*/
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.
- *
- * @return false unless configured otherwise
- */
- boolean disableSaving();
-
- /**
- * Gets the active ISilentContainer implementation.
- *
- * @return the ISilentContainer
- * @throws IllegalStateException if the server version is unsupported
- */
- @NotNull IAnySilentContainer getAnySilentContainer();
-
- /**
- * Gets the active IInventoryAccess implementation.
- *
- * @return the IInventoryAccess
- * @throws IllegalStateException if the server version is unsupported
- */
- @Deprecated
- default @NotNull IInventoryAccess getInventoryAccess() {
- return new InventoryAccess();
- }
-
- /**
- * Gets the provided player's AnyChest setting.
- *
- * @param player the OfflinePlayer
- * @return true if AnyChest is enabled
- * @throws IllegalStateException if the server version is unsupported
- */
- boolean getPlayerAnyChestStatus(@NotNull OfflinePlayer player);
-
- /**
- * Gets a unique identifier by which the OfflinePlayer can be referenced. Using the value
- * returned to look up a Player will generally be much faster for later implementations.
- *
- * @param offline the OfflinePlayer
- * @return the identifier
- * @throws IllegalStateException if the server version is unsupported
- */
- default @NotNull String getPlayerID(@NotNull OfflinePlayer offline) {
- return offline.getUniqueId().toString();
- }
-
- /**
- * Gets a player's SilentChest setting.
- *
- * @param offline the OfflinePlayer
- * @return true if SilentChest is enabled
- * @throws IllegalStateException if the server version is unsupported
- */
- boolean getPlayerSilentChestStatus(@NotNull OfflinePlayer offline);
-
- /**
- * Gets an ISpecialEnderChest for the given Player.
- *
- * @param player the Player
- * @param online true if the Player is currently online
- * @return the ISpecialEnderChest
- * @throws IllegalStateException if the server version is unsupported
- * @throws InstantiationException if the ISpecialEnderChest could not be instantiated
- */
- @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull Player player, boolean online) throws InstantiationException;
-
- /**
- * Gets an ISpecialPlayerInventory for the given Player.
- *
- * @param player the Player
- * @param online true if the Player is currently online
- * @return the ISpecialPlayerInventory
- * @throws IllegalStateException if the server version is unsupported
- * @throws InstantiationException if the ISpecialPlayerInventory could not be instantiated
- */
- @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull Player player, boolean online) throws InstantiationException;
-
- /**
- * Checks if the server version is supported by OpenInv.
- *
- * @return true if the server version is supported
- */
- boolean isSupportedVersion();
-
- /**
- * Load a Player from an OfflinePlayer. May return null under some circumstances.
- *
- * @param offline the OfflinePlayer to load a Player for
- * @return the Player, or null
- * @throws IllegalStateException if the server version is unsupported
- */
- @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline);
-
- /**
- * Get an OfflinePlayer by name.
- *
- * Note: This method is potentially very heavily blocking. It should not ever be called on the
- * main thread, and if it is, a stack trace will be displayed alerting server owners to the
- * call.
- *
- * @param name the name of the Player
- * @return the OfflinePlayer with the closest matching name or null if no players have ever logged in
- */
- default @Nullable OfflinePlayer matchPlayer(@NotNull String name) {
-
- // Warn if called on the main thread - if we resort to searching offline players, this may take several seconds.
- if (Bukkit.getServer().isPrimaryThread()) {
- this.getLogger().warning("Call to OpenInv#matchPlayer made on the main thread!");
- this.getLogger().warning("This can cause the server to hang, potentially severely.");
- this.getLogger().warning("Trace:");
- for (StackTraceElement element : new Throwable().fillInStackTrace().getStackTrace()) {
- this.getLogger().warning(element.toString());
- }
- }
-
- OfflinePlayer player;
-
- try {
- UUID uuid = UUID.fromString(name);
- player = Bukkit.getOfflinePlayer(uuid);
- // Ensure player is a real player, otherwise return null
- if (player.hasPlayedBefore() || player.isOnline()) {
- return player;
- }
- } catch (IllegalArgumentException ignored) {
- // Not a UUID
- }
-
- // Ensure name is valid if server is in online mode to avoid unnecessary searching
- if (Bukkit.getServer().getOnlineMode() && !name.matches("[a-zA-Z0-9_]{3,16}")) {
- return null;
- }
-
- player = Bukkit.getServer().getPlayerExact(name);
-
- if (player != null) {
- return player;
- }
-
- player = Bukkit.getServer().getOfflinePlayer(name);
-
- if (player.hasPlayedBefore()) {
- return player;
- }
-
- player = Bukkit.getServer().getPlayer(name);
-
- if (player != null) {
- return player;
- }
-
- float bestMatch = 0;
- for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) {
- if (offline.getName() == null) {
- // Loaded by UUID only, name has never been looked up.
- continue;
- }
-
- float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName());
-
- if (currentMatch == 1.0F) {
- return offline;
- }
-
- if (currentMatch > bestMatch) {
- bestMatch = currentMatch;
- player = offline;
- }
- }
-
- // Only null if no players have played ever, otherwise even the worst match will do.
- return player;
- }
-
- /**
- * Open an ISpecialInventory for a Player.
- *
- * @param player the Player
- * @param inventory the ISpecialInventory
- * @return the InventoryView for the opened ISpecialInventory
- */
- @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
-
- /**
- * Check the configuration value for whether or not OpenInv displays a notification to the user
- * when a container is activated with AnyChest.
- *
- * @return true unless configured otherwise
- */
- boolean notifyAnyChest();
-
- /**
- * 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
- */
- boolean notifySilentChest();
-
- /**
- * 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
- */
- 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.
- *
- * @param player the Player
- * @param plugin the Plugin holding the reference to the Player
- * @throws IllegalStateException if the server version is unsupported
- */
- void retainPlayer(@NotNull Player player, @NotNull Plugin plugin);
-
- /**
- * Sets a player's AnyChest setting.
- *
- * @param offline the OfflinePlayer
- * @param status the status
- * @throws IllegalStateException if the server version is unsupported
- */
- void setPlayerAnyChestStatus(@NotNull OfflinePlayer offline, boolean status);
-
- /**
- * Sets a player's SilentChest setting.
- *
- * @param offline the OfflinePlayer
- * @param status the status
- * @throws IllegalStateException if the server version is unsupported
- */
- void setPlayerSilentChestStatus(@NotNull OfflinePlayer offline, boolean status);
-
- /**
- * Forcibly unload a cached Player's data.
- *
- * @param offline the OfflinePlayer to unload
- * @throws IllegalStateException if the server version is unsupported
- */
- void unload(@NotNull OfflinePlayer offline);
-
- Logger getLogger();
+ /**
+ * Check if the server version is supported by OpenInv.
+ *
+ * @return true if the server version is supported
+ */
+ boolean isSupportedVersion();
+
+ /**
+ * 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. If true, OpenInv will not load or allow
+ * modification of players while they are not online. This does not prevent other plugins from using existing loaded
+ * players who have gone offline.
+ *
+ * @return false unless configured otherwise
+ * @since 4.2.0
+ */
+ boolean disableOfflineAccess();
+
+ /**
+ * Check the configuration value for whether OpenInv uses history for opening commands. If false, OpenInv will use
+ * the previous parameterized search when no parameters are provided.
+ *
+ * @return false unless configured otherwise
+ * @since 4.3.0
+ */
+ boolean noArgsOpensSelf();
+
+ /**
+ * Get the active {@link IAnySilentContainer} implementation.
+ *
+ * @return the active implementation for the server version
+ * @throws IllegalStateException if the server version is unsupported
+ */
+ @NotNull IAnySilentContainer getAnySilentContainer();
+
+ /**
+ * Get whether a user has AnyContainer mode enabled.
+ *
+ * @param offline the user to obtain the state of
+ * @return true if AnyContainer mode is enabled
+ */
+ boolean getAnyContainerStatus(@NotNull OfflinePlayer offline);
+
+ /**
+ * Set whether a user has AnyContainer mode enabled.
+ *
+ * @param offline the user to set the state of
+ * @param status the state of the mode
+ */
+ void setAnyContainerStatus(@NotNull OfflinePlayer offline, boolean status);
+
+ /**
+ * Get whether a user has SilentContainer mode enabled.
+ *
+ * @param offline the user to obtain the state of
+ * @return true if SilentContainer mode is enabled
+ */
+ boolean getSilentContainerStatus(@NotNull OfflinePlayer offline);
+
+ /**
+ * Set whether a user has SilentContainer mode enabled.
+ *
+ * @param offline the user to set the state of
+ * @param status the state of the mode
+ */
+ void setSilentContainerStatus(@NotNull OfflinePlayer offline, boolean status);
+
+ /**
+ * Get an {@link ISpecialEnderChest} for a user.
+ *
+ * @param player the {@link Player} owning the inventory
+ * @param online whether the owner is currently online
+ * @return the created inventory
+ * @throws IllegalStateException if the server version is unsupported
+ * @throws InstantiationException if there was an issue creating the inventory
+ */
+ @NotNull ISpecialEnderChest getSpecialEnderChest(
+ @NotNull Player player,
+ boolean online
+ ) throws InstantiationException;
+
+ /**
+ * Get an {@link ISpecialPlayerInventory} for a user.
+ *
+ * @param player the {@link Player} owning the inventory
+ * @param online whether the owner is currently online
+ * @return the created inventory
+ * @throws IllegalStateException if the server version is unsupported
+ * @throws InstantiationException if there was an issue creating the inventory
+ */
+ @NotNull ISpecialPlayerInventory getSpecialInventory(
+ @NotNull Player player,
+ boolean online
+ ) throws InstantiationException;
+
+ /**
+ * @deprecated Use {@link #openInventory(Player, ISpecialInventory, boolean)}
+ */
+ @Deprecated(forRemoval = true, since = "5.2.0")
+ @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
+
+ /**
+ * Open an {@link ISpecialInventory} for a {@link Player}.
+ *
+ * @param player the viewer
+ * @param inventory the inventory to open
+ * @param viewOnly whether the inventory should be view-only
+ * @return the resulting {@link InventoryView}
+ */
+ @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly);
+
+ /**
+ * 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 {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for
+ * the server is not loaded, this will return {@code null}.
+ *
+ * @param offline the {@code OfflinePlayer} to load a {@code Player} for
+ * @return the loaded {@code Player}
+ * @throws IllegalStateException if the server version is unsupported
+ */
+ @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline);
+
+ /**
+ * Match an existing {@link OfflinePlayer}. If the name is a {@link UUID#toString() UUID string}, this will only
+ * return the user if they have actually played on the server before, unlike {@link Bukkit#getOfflinePlayer(UUID)}.
+ *
+ *
This method is potentially very heavily blocking. It should not ever be called on the
+ * main thread, and if it is, a stack trace will be displayed alerting server owners to the
+ * call.
+ *
+ * @param name the string to match
+ * @return the user with the closest matching name
+ */
+ @Nullable OfflinePlayer matchPlayer(@NotNull String name);
+
+ /**
+ * Forcibly close inventories of and unload any cached data for a user.
+ *
+ * @param offline the {@link OfflinePlayer} to unload
+ */
+ void unload(@NotNull OfflinePlayer offline);
+
+ Logger getLogger();
}
diff --git a/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java b/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java
new file mode 100644
index 00000000..7f0e16c6
--- /dev/null
+++ b/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java
@@ -0,0 +1,56 @@
+package com.lishid.openinv.event;
+
+import com.google.errorprone.annotations.RestrictedApi;
+import com.lishid.openinv.internal.ISpecialInventory;
+import org.bukkit.entity.Player;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Event fired before OpenInv saves a player's data when closing an {@link ISpecialInventory}.
+ */
+public class OpenPlayerSaveEvent extends PlayerSaveEvent {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final ISpecialInventory inventory;
+
+ /**
+ * Construct a new {@code OpenPlayerSaveEvent}.
+ *
+ *
The constructor is not considered part of the API, and may be subject to change.
+ *
+ * @param player the player to be saved
+ * @param inventory the {@link ISpecialInventory} being closed
+ */
+ @RestrictedApi(
+ explanation = "Constructor is not considered part of the API and may be subject to change.",
+ allowedOnPath = ".*/com/lishid/openinv/event/OpenEvents.java"
+ )
+ @ApiStatus.Internal
+ OpenPlayerSaveEvent(@NotNull Player player, @NotNull ISpecialInventory inventory) {
+ super(player);
+ this.inventory = inventory;
+ }
+
+ /**
+ * Get the {@link ISpecialInventory} that triggered the save by being closed.
+ *
+ * @return the special inventory
+ */
+ public @NotNull ISpecialInventory getInventory() {
+ return inventory;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java b/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java
new file mode 100644
index 00000000..3fd98039
--- /dev/null
+++ b/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java
@@ -0,0 +1,67 @@
+package com.lishid.openinv.event;
+
+
+import com.google.errorprone.annotations.RestrictedApi;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.HandlerList;
+import org.bukkit.event.player.PlayerEvent;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Event fired before a {@link Player} loaded via OpenInv is saved.
+ */
+public class PlayerSaveEvent extends PlayerEvent implements Cancellable {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private boolean cancelled = false;
+
+ /**
+ * Construct a new {@code PlayerSaveEvent}.
+ *
+ *
The constructor is not considered part of the API, and may be subject to change.
+ *
+ * @param player the player to be saved
+ */
+ @RestrictedApi(
+ explanation = "Constructor is not considered part of the API and may be subject to change.",
+ allowedOnPath = ".*/com/lishid/openinv/event/(OpenPlayerSaveEvent|OpenEvents).java"
+ )
+ @ApiStatus.Internal
+ PlayerSaveEvent(@NotNull Player player) {
+ super(player);
+ }
+
+ /**
+ * Get whether the event is cancelled.
+ *
+ * @return true if the event is cancelled
+ */
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ /**
+ * Set whether the event is cancelled.
+ *
+ * @param cancel whether the event is cancelled
+ */
+ @Override
+ public void setCancelled(boolean cancel) {
+ this.cancelled = cancel;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java b/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java
new file mode 100644
index 00000000..ed00a5ee
--- /dev/null
+++ b/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java
@@ -0,0 +1,71 @@
+package com.lishid.openinv.event;
+
+import com.google.errorprone.annotations.RestrictedApi;
+import com.lishid.openinv.util.setting.PlayerToggle;
+import org.bukkit.event.Event;
+import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Event fired after OpenInv modifies a toggleable setting for a player.
+ */
+public class PlayerToggledEvent extends Event {
+
+ private static final HandlerList HANDLERS = new HandlerList();
+
+ private final @NotNull PlayerToggle toggle;
+ private final @NotNull UUID uuid;
+ private final boolean enabled;
+
+ @RestrictedApi(
+ explanation = "Constructor is not considered part of the API and may be subject to change.",
+ allowedOnPath = ".*/com/lishid/openinv/event/OpenEvents.java"
+ )
+ @ApiStatus.Internal
+ PlayerToggledEvent(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean enabled) {
+ this.toggle = toggle;
+ this.uuid = uuid;
+ this.enabled = enabled;
+ }
+
+ /**
+ * Get the {@link PlayerToggle} affected.
+ *
+ * @return the toggle
+ */
+ public @NotNull PlayerToggle getToggle() {
+ return toggle;
+ }
+
+ /**
+ * Get the {@link UUID} of the player whose setting was changed.
+ *
+ * @return the player ID
+ */
+ public @NotNull UUID getPlayerId() {
+ return uuid;
+ }
+
+ /**
+ * Get whether the toggle is enabled.
+ *
+ * @return true if the toggle is enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @NotNull
+ @Override
+ public HandlerList getHandlers() {
+ return HANDLERS;
+ }
+
+ public static HandlerList getHandlerList() {
+ return HANDLERS;
+ }
+
+}
diff --git a/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java b/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java
index 02cab779..3f83147a 100644
--- a/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java
+++ b/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 lishid. All rights reserved.
+ * Copyright (C) 2011-2023 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,45 +17,106 @@
package com.lishid.openinv.internal;
import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.EnderChest;
+import org.bukkit.block.data.Directional;
+import org.bukkit.entity.Cat;
import org.bukkit.entity.Player;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.util.BoundingBox;
import org.jetbrains.annotations.NotNull;
public interface IAnySilentContainer {
- /**
- * Opens the container at the given coordinates for the Player. If you do not want blocked
- * containers to open, be sure to check {@link #isAnyContainerNeeded(Player, Block)}
- * first.
- *
- * @param player the Player opening the container
- * @param silent whether the container's noise is to be silenced
- * @param block the Block
- * @return true if the container can be opened
- */
- boolean activateContainer(@NotNull Player player, boolean silent, @NotNull Block block);
-
- /**
- * Closes the Player's currently open container silently, if necessary.
- *
- * @param player the Player closing a container
- */
- void deactivateContainer(@NotNull Player player);
-
- /**
- * Checks if the container at the given coordinates is blocked.
- *
- * @param player the Player opening the container
- * @param block the Block
- * @return true if the container is blocked
- */
- boolean isAnyContainerNeeded(@NotNull Player player, @NotNull Block block);
-
- /**
- * Checks if the given block is a container which can be unblocked or silenced.
- *
- * @param block the BlockState
- * @return true if the Block is a supported container
- */
- boolean isAnySilentContainer(@NotNull Block block);
+ /**
+ * Forcibly open the container at the given coordinates for the Player. This will open blocked containers! Be sure
+ * to check {@link #isAnyContainerNeeded(Block)} first if that is not desirable.
+ *
+ * @param player the {@link Player} opening the container
+ * @param silent whether the container's noise is to be silenced
+ * @param block the {@link Block} of the container
+ * @return true if the container can be opened
+ */
+ boolean activateContainer(@NotNull Player player, boolean silent, @NotNull Block block);
+
+ /**
+ * Perform operations required to close the current container silently.
+ *
+ * @param player the {@link Player} closing a container
+ */
+ void deactivateContainer(@NotNull Player player);
+
+ /**
+ * Check if the container at the given coordinates is blocked.
+ *
+ * @param block the {@link Block} of the container
+ * @return true if the container is blocked
+ */
+ boolean isAnyContainerNeeded(@NotNull Block block);
+
+ /**
+ * Check if a shulker box block cannot be opened under ordinary circumstances.
+ *
+ * @param shulkerBox the shulker box block
+ * @return whether the container is blocked
+ */
+ default boolean isShulkerBlocked(@NotNull Block shulkerBox) {
+ Directional directional = (Directional) shulkerBox.getBlockData();
+ BlockFace facing = directional.getFacing();
+ // Construct a new 1-block bounding box at the origin.
+ BoundingBox box = new BoundingBox(0, 0, 0, 1, 1, 1);
+ // Expand the box in the direction the shulker will open.
+ box.expand(facing, 0.5);
+ // Move the box away from the origin by a block so only the expansion intersects with a box around the origin.
+ box.shift(facing.getOppositeFace().getDirection());
+ // Check if the relative block's collision shape (which will be at the origin) intersects with the expanded box.
+ return shulkerBox.getRelative(facing).getCollisionShape().overlaps(box);
+ }
+
+ /**
+ * Check if a chest cannot be opened under ordinary circumstances.
+ *
+ * @param chest the chest block
+ * @return whether the container is blocked
+ */
+ default boolean isChestBlocked(@NotNull Block chest) {
+ org.bukkit.block.Block relative = chest.getRelative(0, 1, 0);
+ return relative.getType().isOccluding()
+ || !chest.getWorld().getNearbyEntities(BoundingBox.of(relative), Cat.class::isInstance).isEmpty();
+ }
+
+ /**
+ * Check if the given {@link Block} is a container which can be unblocked or silenced.
+ *
+ * @param block the potential container
+ * @return true if the type is a supported container
+ */
+ boolean isAnySilentContainer(@NotNull Block block);
+
+ /**
+ * Check if the given {@link BlockState} is a container which can be unblocked or silenced.
+ *
+ * @param blockState the potential container
+ * @return true if the type is a supported container
+ */
+ default boolean isAnySilentContainer(@NotNull BlockState blockState) {
+ return (blockState instanceof InventoryHolder holder && isAnySilentContainer(holder))
+ || blockState instanceof EnderChest;
+ }
+
+ /**
+ * Check if the given {@link InventoryHolder} is a container which can be unblocked or silenced.
+ *
+ * @param holder the potential container
+ * @return true if the type is a supported container
+ */
+ default boolean isAnySilentContainer(@NotNull InventoryHolder holder) {
+ return holder instanceof org.bukkit.block.EnderChest
+ || holder instanceof org.bukkit.block.Chest
+ || holder instanceof org.bukkit.block.DoubleChest
+ || holder instanceof org.bukkit.block.ShulkerBox
+ || holder instanceof org.bukkit.block.Barrel;
+ }
}
diff --git a/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java b/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java
deleted file mode 100644
index 37677047..00000000
--- a/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal;
-
-import org.bukkit.inventory.Inventory;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-@Deprecated
-public interface IInventoryAccess {
-
- /**
- * Gets an ISpecialEnderChest from an Inventory or null if the Inventory is not backed by an
- * ISpecialEnderChest.
- *
- * @param inventory the Inventory
- * @return the ISpecialEnderChest or null
- */
- @Deprecated
- @Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory);
-
- /**
- * Gets an ISpecialPlayerInventory from an Inventory or null if the Inventory is not backed by
- * an ISpecialPlayerInventory.
- *
- * @param inventory the Inventory
- * @return the ISpecialPlayerInventory or null
- */
- @Deprecated
- @Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory);
-
- /**
- * Check if an Inventory is an ISpecialEnderChest implementation.
- *
- * @param inventory the Inventory
- * @return true if the Inventory is backed by an ISpecialEnderChest
- */
- @Deprecated
- boolean isSpecialEnderChest(@NotNull Inventory inventory);
-
- /**
- * Check if an Inventory is an ISpecialPlayerInventory implementation.
- *
- * @param inventory the Inventory
- * @return true if the Inventory is backed by an ISpecialPlayerInventory
- */
- @Deprecated
- boolean isSpecialPlayerInventory(@NotNull Inventory inventory);
-
-}
diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java
index 86657b05..4b35c4c8 100644
--- a/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java
+++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 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,6 +16,17 @@
package com.lishid.openinv.internal;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An {@link ISpecialInventory} representing an ender chest.
+ */
public interface ISpecialEnderChest extends ISpecialInventory {
+ @Override
+ default @NotNull InventoryType getBukkitType() {
+ return InventoryType.ENDER_CHEST;
+ }
+
}
diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java
index e6929c96..2793526d 100644
--- a/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java
+++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 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,36 +16,60 @@
package com.lishid.openinv.internal;
+import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
+/**
+ * Interface defining behavior for special inventories backed by other inventories' content listings.
+ */
public interface ISpecialInventory {
- /**
- * Gets the Inventory associated with this ISpecialInventory.
- *
- * @return the Inventory
- */
- @NotNull Inventory getBukkitInventory();
-
- /**
- * Sets the Player associated with this ISpecialInventory online.
- *
- * @param player the Player coming online
- */
- void setPlayerOnline(@NotNull Player player);
-
- /**
- * Sets the Player associated with this ISpecialInventory offline.
- */
- void setPlayerOffline();
-
- /**
- * Gets whether or not this ISpecialInventory is in use.
- *
- * @return true if the ISpecialInventory is in use
- */
- boolean isInUse();
+ /**
+ * Get the {@link Inventory} associated with this {@code ISpecialInventory}.
+ *
+ * @return the Bukkit inventory
+ */
+ @NotNull Inventory getBukkitInventory();
+
+ /**
+ * Get the {@link InventoryType} corresponding to this {@code ISpecialInventory}.
+ *
+ * @return the type of Bukkit inventory
+ */
+ @NotNull InventoryType getBukkitType();
+
+ /**
+ * Set the owning {@link Player} instance to a newly-joined user.
+ *
+ * @param player the user coming online
+ */
+ void setPlayerOnline(@NotNull Player player);
+
+ /**
+ * Mark the owner of the inventory offline.
+ *
+ * @deprecated No longer used by implementations.
+ */
+ @Deprecated(forRemoval = true, since = "5.1.11")
+ default void setPlayerOffline() {}
+
+ /**
+ * Get whether the inventory is being viewed by any users.
+ *
+ * @return true if the inventory is being viewed
+ */
+ default boolean isInUse() {
+ return !getBukkitInventory().getViewers().isEmpty();
+ }
+
+ /**
+ * Get the {@link Player} who owns the inventory.
+ *
+ * @return the {@link HumanEntity} who owns the inventory
+ */
+ @NotNull HumanEntity getPlayer();
}
diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java
index f06f724f..91affaf8 100644
--- a/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java
+++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 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,6 +16,17 @@
package com.lishid.openinv.internal;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An {@link ISpecialInventory} representing a player inventory.
+ */
public interface ISpecialPlayerInventory extends ISpecialInventory {
+ @Override
+ default @NotNull InventoryType getBukkitType() {
+ return InventoryType.PLAYER;
+ }
+
}
diff --git a/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java b/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java
index 9c2ffd72..6b3aa005 100644
--- a/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java
+++ b/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 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,119 +16,91 @@
package com.lishid.openinv.util;
-import com.lishid.openinv.internal.IInventoryAccess;
+import com.google.errorprone.annotations.RestrictedApi;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import org.bukkit.Bukkit;
import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-public class InventoryAccess implements IInventoryAccess {
-
- private static Class> craftInventory = null;
- private static Method getInventory = null;
-
- static {
- String packageName = Bukkit.getServer().getClass().getPackage().getName();
- try {
- craftInventory = Class.forName(packageName + ".inventory.CraftInventory");
- } catch (ClassNotFoundException ignored) {}
- try {
- getInventory = craftInventory.getDeclaredMethod("getInventory");
- } catch (NoSuchMethodException ignored) {}
- }
-
- /**
- * @deprecated use {@link #isUsable()}
- */
- @Deprecated
- public static boolean isUseable() {
- return isUsable();
- }
-
- public static boolean isUsable() {
- return craftInventory != null && getInventory != null;
- }
-
- public static boolean isPlayerInventory(@NotNull Inventory inventory) {
- return getPlayerInventory(inventory) != null;
- }
-
- public static @Nullable ISpecialPlayerInventory getPlayerInventory(@NotNull Inventory inventory) {
- return getSpecialInventory(ISpecialPlayerInventory.class, inventory);
- }
-
- public static boolean isEnderChest(@NotNull Inventory inventory) {
- return getEnderChest(inventory) != null;
- }
-
- public static @Nullable ISpecialEnderChest getEnderChest(@NotNull Inventory inventory) {
- return getSpecialInventory(ISpecialEnderChest.class, inventory);
- }
-
- private static @Nullable T getSpecialInventory(@NotNull Class expected, @NotNull Inventory inventory) {
- Object inv;
- if (craftInventory != null && getInventory != null && craftInventory.isAssignableFrom(inventory.getClass())) {
- try {
- inv = getInventory.invoke(inventory);
- if (expected.isInstance(inv)) {
- return expected.cast(inv);
- }
- } catch (ReflectiveOperationException ignored) {}
- }
-
- inv = grabFieldOfTypeFromObject(expected, inventory);
-
- if (expected.isInstance(inv)) {
- return expected.cast(inv);
- }
-
- return null;
- }
-
- private static @Nullable T grabFieldOfTypeFromObject(final Class type, final Object object) {
- // Use reflection to find the IInventory
- Class> clazz = object.getClass();
- T result = null;
- for (Field f : clazz.getDeclaredFields()) {
- f.setAccessible(true);
- if (type.isAssignableFrom(f.getDeclaringClass())) {
- try {
- result = type.cast(f.get(object));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- return result;
- }
-
- @Deprecated
- @Override
- public @Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory) {
- return getEnderChest(inventory);
- }
-
- @Deprecated
- @Override
- public @Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory) {
- return getPlayerInventory(inventory);
- }
-
- @Deprecated
- @Override
- public boolean isSpecialEnderChest(@NotNull Inventory inventory) {
- return isEnderChest(inventory);
- }
-
- @Deprecated
- @Override
- public boolean isSpecialPlayerInventory(@NotNull Inventory inventory) {
- return isPlayerInventory(inventory);
- }
+import java.util.function.BiFunction;
+
+public final class InventoryAccess {
+
+ private static @Nullable BiFunction, ISpecialInventory> provider;
+
+ public static boolean isUsable() {
+ return provider != null;
+ }
+
+ /**
+ * Check if an {@link Inventory} is an {@link ISpecialPlayerInventory} implementation.
+ *
+ * @param inventory the Bukkit inventory
+ * @return true if backed by the correct implementation
+ */
+ public static boolean isPlayerInventory(@NotNull Inventory inventory) {
+ return getPlayerInventory(inventory) != null;
+ }
+
+ /**
+ * Get the {@link ISpecialPlayerInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is
+ * not backed by the correct class.
+ *
+ * @param inventory the Bukkit inventory
+ * @return the backing implementation if available
+ */
+ public static @Nullable ISpecialPlayerInventory getPlayerInventory(@NotNull Inventory inventory) {
+ return provider == null ? null : (ISpecialPlayerInventory) provider.apply(inventory, ISpecialPlayerInventory.class);
+ }
+
+ /**
+ * Check if an {@link Inventory} is an {@link ISpecialEnderChest} implementation.
+ *
+ * @param inventory the Bukkit inventory
+ * @return true if backed by the correct implementation
+ */
+ public static boolean isEnderChest(@NotNull Inventory inventory) {
+ return getEnderChest(inventory) != null;
+ }
+
+ /**
+ * Get the {@link ISpecialEnderChest} backing an {@link Inventory}. Returns {@code null} if the inventory is
+ * not backed by the correct class.
+ *
+ * @param inventory the Bukkit inventory
+ * @return the backing implementation if available
+ */
+ public static @Nullable ISpecialEnderChest getEnderChest(@NotNull Inventory inventory) {
+ return provider == null ? null : (ISpecialEnderChest) provider.apply(inventory, ISpecialEnderChest.class);
+ }
+
+ /**
+ * Get a {@link ISpecialInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is not backed
+ * by the correct class.
+ *
+ * @param inventory the Bukkit inventory
+ * @return the backing implementation if available
+ */
+ public static @Nullable ISpecialInventory getInventory(@NotNull Inventory inventory) {
+ return provider == null ? null : provider.apply(inventory, ISpecialInventory.class);
+ }
+
+ @RestrictedApi(
+ explanation = "Not part of the API.",
+ allowedOnPath = ".*/com/lishid/openinv/util/InternalAccessor.java"
+ )
+ @ApiStatus.Internal
+ static void setProvider(
+ @Nullable BiFunction, ISpecialInventory> provider
+ ) {
+ InventoryAccess.provider = provider;
+ }
+
+ private InventoryAccess() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
}
diff --git a/api/src/main/java/com/lishid/openinv/util/StringMetric.java b/api/src/main/java/com/lishid/openinv/util/StringMetric.java
deleted file mode 100644
index fe2e4a88..00000000
--- a/api/src/main/java/com/lishid/openinv/util/StringMetric.java
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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;
-
-public class StringMetric {
-
- public static float compareJaroWinkler(String a, String b) {
- final float jaroScore = compareJaro(a, b);
-
- if (jaroScore < (float) 0.7) {
- return jaroScore;
- }
-
- String prefix = commonPrefix(a, b);
- int prefixLength = Math.min(prefix.codePointCount(0, prefix.length()), 4);
-
- return jaroScore + (prefixLength * (float) 0.1 * (1.0f - jaroScore));
-
- }
-
- private static float compareJaro(String a, String b) {
- if (a.isEmpty() && b.isEmpty()) {
- return 1.0f;
- }
-
- if (a.isEmpty() || b.isEmpty()) {
- return 0.0f;
- }
-
- final int[] charsA = a.codePoints().toArray();
- final int[] charsB = b.codePoints().toArray();
-
- // Intentional integer division to round down.
- final int halfLength = Math.max(0, Math.max(charsA.length, charsB.length) / 2 - 1);
-
- final int[] commonA = getCommonCodePoints(charsA, charsB, halfLength);
- final int[] commonB = getCommonCodePoints(charsB, charsA, halfLength);
-
- // commonA and commonB will always contain the same multi-set of
- // characters. Because getCommonCharacters has been optimized, commonA
- // and commonB are -1-padded. So in this loop we count transposition
- // and use commonCharacters to determine the length of the multi-set.
- float transpositions = 0;
- int commonCharacters = 0;
- for (int length = commonA.length; commonCharacters < length
- && commonA[commonCharacters] > -1; commonCharacters++) {
- if (commonA[commonCharacters] != commonB[commonCharacters]) {
- transpositions++;
- }
- }
-
- if (commonCharacters == 0) {
- return 0.0f;
- }
-
- float aCommonRatio = commonCharacters / (float) charsA.length;
- float bCommonRatio = commonCharacters / (float) charsB.length;
- float transpositionRatio = (commonCharacters - transpositions / 2.0f) / commonCharacters;
-
- return (aCommonRatio + bCommonRatio + transpositionRatio) / 3.0f;
- }
-
- /*
- * Returns an array of code points from a within b. A character in b is
- * counted as common when it is within separation distance from the position
- * in a.
- */
- private static int[] getCommonCodePoints(final int[] charsA, final int[] charsB, final int separation) {
- final int[] common = new int[Math.min(charsA.length, charsB.length)];
- final boolean[] matched = new boolean[charsB.length];
-
- // Iterate of string a and find all characters that occur in b within
- // the separation distance. Mark any matches found to avoid
- // duplicate matchings.
- int commonIndex = 0;
- for (int i = 0, length = charsA.length; i < length; i++) {
- final int character = charsA[i];
- final int index = indexOf(character, charsB, i - separation, i
- + separation + 1, matched);
- if (index > -1) {
- common[commonIndex++] = character;
- matched[index] = true;
- }
- }
-
- if (commonIndex < common.length) {
- common[commonIndex] = -1;
- }
-
- // Both invocations will yield the same multi-set terminated by -1, so
- // they can be compared for transposition without making a copy.
- return common;
- }
-
- /*
- * Search for code point in buffer starting at fromIndex to toIndex - 1.
- *
- * Returns -1 when not found.
- */
- private static int indexOf(int character, int[] buffer, int fromIndex, int toIndex, boolean[] matched) {
-
- // compare char with range of characters to either side
- for (int j = Math.max(0, fromIndex), length = Math.min(toIndex, buffer.length); j < length; j++) {
- // check if found
- if (buffer[j] == character && !matched[j]) {
- return j;
- }
- }
-
- return -1;
- }
-
- private static String commonPrefix(CharSequence a, CharSequence b) {
- int maxPrefixLength = Math.min(a.length(), b.length());
-
- int p;
-
- p = 0;
- while (p < maxPrefixLength && a.charAt(p) == b.charAt(p)) {
- ++p;
- }
-
- if (validSurrogatePairAt(a, p - 1) || validSurrogatePairAt(b, p - 1)) {
- --p;
- }
-
- return a.subSequence(0, p).toString();
- }
-
- private static boolean validSurrogatePairAt(CharSequence string, int index) {
- return index >= 0 && index <= string.length() - 2 && Character.isHighSurrogate(string.charAt(index)) && Character.isLowSurrogate(string.charAt(index + 1));
- }
-
- private StringMetric(){}
-
-}
diff --git a/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java
new file mode 100644
index 00000000..6e8dc880
--- /dev/null
+++ b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java
@@ -0,0 +1,36 @@
+package com.lishid.openinv.util.setting;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * A per-player setting that may be enabled or disabled.
+ */
+public interface PlayerToggle {
+
+ /**
+ * Get the name of the setting.
+ *
+ * @return the setting name
+ */
+ @NotNull String getName();
+
+ /**
+ * Get the state of the toggle for a particular player ID.
+ *
+ * @param uuid the player ID
+ * @return true if the setting is enabled
+ */
+ boolean is(@NotNull UUID uuid);
+
+ /**
+ * Set the state of the toggle for a particular player ID.
+ *
+ * @param uuid the player ID
+ * @param enabled whether the setting is enabled
+ * @return true if the setting changed as a result of being set
+ */
+ boolean set(@NotNull UUID uuid, boolean enabled);
+
+}
diff --git a/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java
new file mode 100644
index 00000000..d61cd115
--- /dev/null
+++ b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java
@@ -0,0 +1,106 @@
+package com.lishid.openinv.util.setting;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.UnmodifiableView;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Utility class containing all of OpenInv's {@link PlayerToggle PlayerToggles}.
+ */
+public final class PlayerToggles {
+
+ private static final Map TOGGLES = new HashMap<>();
+ private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer"));
+ private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer"));
+
+ /**
+ * Get the AnyContainer toggle.
+ *
+ * @return the AnyContainer toggle
+ */
+ public static @NotNull PlayerToggle any() {
+ return ANY;
+ }
+
+ /**
+ * Get the SilentContainer toggle.
+ *
+ * @return the SilentContainer toggle
+ */
+ public static @NotNull PlayerToggle silent() {
+ return SILENT;
+ }
+
+ /**
+ * Get a toggle by name.
+ *
+ * @param toggleName the name of the toggle
+ * @return the toggle, or null if no such toggle exists.
+ */
+ public static @Nullable PlayerToggle get(@NotNull String toggleName) {
+ PlayerToggle toggle = TOGGLES.get(toggleName);
+ if (toggle == null) {
+ toggle = TOGGLES.get(toggleName.toLowerCase(Locale.ENGLISH));
+ }
+ return toggle;
+ }
+
+ /**
+ * Get an unmodifable view of all toggles available.
+ *
+ * @return a view of all toggles available
+ */
+ public static @UnmodifiableView @NotNull Collection get() {
+ return Collections.unmodifiableCollection(TOGGLES.values());
+ }
+
+ private static @NotNull PlayerToggle add(@NotNull PlayerToggle toggle) {
+ TOGGLES.put(toggle.getName().toLowerCase(Locale.ENGLISH), toggle);
+ return toggle;
+ }
+
+ private PlayerToggles() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
+
+ private static class MemoryToggle implements PlayerToggle {
+
+ private final @NotNull Set enabled;
+ private final @NotNull String name;
+
+ private MemoryToggle(@NotNull String name) {
+ enabled = new HashSet<>();
+ this.name = name;
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return this.name;
+ }
+
+ @Override
+ public boolean is(@NotNull UUID uuid) {
+ return enabled.contains(uuid);
+ }
+
+ @Override
+ public boolean set(@NotNull UUID uuid, boolean enabled) {
+ if (enabled) {
+ return this.enabled.add(uuid);
+ } else {
+ return this.enabled.remove(uuid);
+ }
+ }
+
+ }
+
+}
diff --git a/assembly/pom.xml b/assembly/pom.xml
deleted file mode 100644
index 421f77ec..00000000
--- a/assembly/pom.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
- 4.0.0
-
-
- com.lishid
- openinvparent
- 4.1.6-SNAPSHOT
-
-
- openinvassembly
- OpenInvAssembly
-
-
- ../target
- OpenInv
-
-
-
- maven-assembly-plugin
- 3.2.0
-
-
- reactor-uberjar
- package
-
- single
-
-
- false
-
- src/assembly/reactor-uberjar.xml
-
-
-
-
-
-
-
-
-
-
diff --git a/assembly/src/assembly/reactor-uberjar.xml b/assembly/src/assembly/reactor-uberjar.xml
deleted file mode 100644
index 36beb33e..00000000
--- a/assembly/src/assembly/reactor-uberjar.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
- reactor-uberjar
-
-
- jar
-
-
- false
-
-
-
-
- true
-
-
- /
- true
-
-
-
-
-
-
-
-
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 00000000..2aa1d9f4
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+ `java-library`
+ alias(libs.plugins.paperweight) apply false
+ alias(libs.plugins.shadow) apply false
+ id(libs.plugins.errorprone.gradle.get().pluginId) apply false
+}
+
+// Set by Spigot module, used by Paper module to convert to Spigot's version of Mojang mappings.
+project.ext.set("craftbukkitPackage", "UNKNOWN")
+
+repositories {
+ maven("https://repo.papermc.io/repository/maven-public/")
+}
+
+// Allow submodules to target higher Java release versions.
+// Not currently necessary (as lowest supported version is in the 1.21 range)
+// but may become relevant in the future.
+java.disableAutoTargetJvm()
+
+// Task to delete ./dist where final files are output.
+tasks.register("cleanDist") {
+ delete("dist")
+}
+
+tasks.clean {
+ // Also delete distribution folder when cleaning.
+ dependsOn(tasks.named("cleanDist"))
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 00000000..aef4e143
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ gradlePluginPortal()
+ mavenCentral()
+}
+
+dependencies {
+ val libs = project.extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
+ implementation(libs.findLibrary("specialsource").orElseThrow())
+ implementation(libs.findLibrary("errorprone-gradle").orElseThrow())
+}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
new file mode 100644
index 00000000..215a5d58
--- /dev/null
+++ b/buildSrc/settings.gradle.kts
@@ -0,0 +1,7 @@
+dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt
new file mode 100644
index 00000000..2bedf878
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt
@@ -0,0 +1,100 @@
+package com.github.jikoo.openinv
+
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.ValueSource
+import org.gradle.api.provider.ValueSourceParameters
+import org.gradle.process.ExecOperations
+import java.io.File
+import java.net.URI
+import java.nio.file.Files
+import javax.inject.Inject
+
+abstract class BuildToolsValueSource : ValueSource {
+
+ @get:Inject
+ abstract val exec: ExecOperations
+
+ interface Parameters : ValueSourceParameters {
+ val mavenLocal: Property
+ val workingDir: DirectoryProperty
+
+ val spigotVersion: Property
+ val spigotRevision: Property
+
+ val ignoreCached: Property
+
+ val javaHome: DirectoryProperty
+ val javaExecutable: Property
+ }
+
+ override fun obtain(): File {
+ val version = parameters.spigotVersion.get()
+ val revision = parameters.spigotRevision.get()
+ val installLocation = getInstallLocation(version)
+ // If Spigot is already installed, don't reinstall.
+ if (!parameters.ignoreCached.get() && installLocation.exists()) {
+ println("Skipping Spigot installation, $version is present")
+ return installLocation
+ }
+
+ val buildTools = installBuildTools(parameters.workingDir.get().asFile)
+
+ println("Installing Spigot $version (rev $revision)")
+
+ exec.javaexec {
+ environment["JAVA_HOME"] = parameters.javaHome.get()
+ executable = parameters.javaExecutable.get()
+ workingDir = buildTools.parentFile
+ classpath(buildTools)
+ args = listOf("--nogui", "--rev", revision, "--remapped")
+ }.rethrowFailure()
+
+ // Mark work for delete.
+ cleanUp(buildTools.parentFile)
+
+ if (!installLocation.exists()) {
+ throw IllegalStateException(
+ "Failed to install Spigot $version from $revision. Does the revision point to a different version?"
+ )
+ }
+ return installLocation
+ }
+
+ private fun getInstallLocation(version: String): File {
+ return parameters.mavenLocal.get().resolve("org/spigotmc/spigot/$version/spigot-$version-remapped-mojang.jar")
+ }
+
+ private fun installBuildTools(workingDir: File): File {
+ val buildTools = workingDir.resolve("BuildTools.jar")
+ if (buildTools.exists()) {
+ return buildTools
+ }
+
+ workingDir.mkdirs()
+
+ val buildToolsUrl =
+ "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar"
+ println("Downloading $buildToolsUrl")
+ val stream = URI.create(buildToolsUrl).toURL().openStream()
+ Files.copy(stream, buildTools.toPath())
+ stream.close()
+
+ return buildTools
+ }
+
+ private fun cleanUp(dir: File) {
+ dir.deleteOnExit()
+ if (!dir.isDirectory) {
+ return
+ }
+
+ dir.listFiles()?.forEach {
+ if (it.isDirectory) {
+ cleanUp(it)
+ } else {
+ it.deleteOnExit()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt
new file mode 100644
index 00000000..94e82f47
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt
@@ -0,0 +1,21 @@
+package com.github.jikoo.openinv
+
+import org.gradle.api.model.ObjectFactory
+import org.gradle.jvm.toolchain.JavaToolchainSpec
+
+abstract class SpigotDependencyExtension(
+ objects: ObjectFactory
+) {
+
+ val version = objects.property(String::class.java)
+ val revision = objects.property(String::class.java)
+ .convention(version.map {
+ it.replace("-R\\d+\\.\\d+-SNAPSHOT".toRegex(), "")
+ })
+ val configuration = objects.property(String::class.java)
+ val classifier = objects.property(String::class.java).convention("remapped-mojang")
+ val ext = objects.property(String::class.java)
+ val java = objects.property(JavaToolchainSpec::class.java)
+ val ignoreCached = objects.property(Boolean::class.java).convention(false)
+
+}
diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt
new file mode 100644
index 00000000..ca12b330
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt
@@ -0,0 +1,56 @@
+package com.github.jikoo.openinv
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.attributes.Bundling
+import org.gradle.api.attributes.Category
+import org.gradle.api.attributes.LibraryElements
+import org.gradle.api.attributes.Usage
+import org.gradle.jvm.tasks.Jar
+import org.gradle.kotlin.dsl.named
+import org.gradle.kotlin.dsl.register
+import java.nio.file.Paths
+
+class SpigotReobf : Plugin {
+
+ companion object {
+ const val ARTIFACT_CONFIG = "reobf"
+ }
+
+ override fun apply(target: Project) {
+ // Re-use extension from Spigot dependency declaration if available to reduce configuration requirements.
+ val spigotExt = target.dependencies.extensions.findByType(SpigotDependencyExtension::class.java)
+ ?: target.dependencies.extensions.create(
+ "spigot",
+ SpigotDependencyExtension::class.java,
+ target.objects
+ )
+
+ val mvnLocal = target.repositories.mavenLocal()
+
+ val reobfTask = target.tasks.register("reobfTask") {
+ dependsOn(target.tasks.named("shadowJar"))
+ // ShadowJar extends Jar, so this should be a safe way to get the result without having
+ // to jump through hoops and shift around shadow declarations in the rest of the project.
+ inputFile.convention(target.tasks.named("shadowJar").get().archiveFile)
+ spigotVersion.convention(spigotExt.version)
+ getMavenLocal().set(Paths.get(mvnLocal.url).toFile())
+ }
+
+ // Set up configuration for producing reobf jar.
+ target.configurations.consumable(ARTIFACT_CONFIG) {
+ attributes {
+ attribute(Category.CATEGORY_ATTRIBUTE, target.objects.named(Category.LIBRARY))
+ attribute(Usage.USAGE_ATTRIBUTE, target.objects.named(Usage.JAVA_RUNTIME))
+ attribute(Bundling.BUNDLING_ATTRIBUTE, target.objects.named(Bundling.EXTERNAL))
+ attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, target.objects.named(LibraryElements.JAR))
+ }
+ }
+
+ // Add artifact from reobf task.
+ target.artifacts {
+ add(ARTIFACT_CONFIG, reobfTask)
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt
new file mode 100644
index 00000000..e9e12ede
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt
@@ -0,0 +1,85 @@
+package com.github.jikoo.openinv
+
+import net.md_5.specialsource.Jar
+import net.md_5.specialsource.JarMapping
+import net.md_5.specialsource.JarRemapper
+import net.md_5.specialsource.RemapperProcessor
+import net.md_5.specialsource.provider.JarProvider
+import net.md_5.specialsource.provider.JointProvider
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+
+abstract class SpigotReobfTask : org.gradle.api.tasks.bundling.Jar() {
+
+ @get:Input
+ val spigotVersion: Property = objectFactory.property(String::class.java)
+
+ @get:InputFile
+ val inputFile: RegularFileProperty = objectFactory.fileProperty()
+
+ @get:Input
+ val intermediaryClassifier: Property = objectFactory.property(String::class.java).convention("mojang-mapped")
+
+ private val mavenLocal: Property = objectFactory.property(File::class.java)
+
+ init {
+ archiveClassifier.convention(SpigotReobf.ARTIFACT_CONFIG)
+ }
+
+ @TaskAction
+ override fun copy() {
+ val spigotVer = spigotVersion.get()
+ val inFile = inputFile.get().asFile
+ val obfPath = inFile.resolveSibling(inFile.name.replace(".jar", "-${intermediaryClassifier.get()}.jar"))
+
+ // https://www.spigotmc.org/threads/510208/#post-4184317
+ val repo = mavenLocal.get()
+ val spigotDir = repo.resolve("org/spigotmc/spigot/$spigotVer/")
+ val mappingDir = repo.resolve("org/spigotmc/minecraft-server/$spigotVer/")
+
+ // Remap original Mojang-mapped jar to obfuscated intermediary
+ val mojangServer = spigotDir.resolve("spigot-$spigotVer-remapped-mojang.jar")
+ val mojangMappings = mappingDir.resolve("minecraft-server-$spigotVer-maps-mojang.txt")
+ remapPartial(mojangServer, mojangMappings, inFile, obfPath, true)
+
+ // Remap obfuscated intermediary jar to Spigot and replace original
+ val obfServer = spigotDir.resolve("spigot-$spigotVer-remapped-obf.jar")
+ val spigotMappings = mappingDir.resolve("minecraft-server-$spigotVer-maps-spigot.csrg")
+ remapPartial(obfServer, spigotMappings, obfPath, archiveFile.get().asFile, false)
+ }
+
+ private fun remapPartial(server: File, mapping: File, input: File, output: File, reverse: Boolean) {
+ val jarMapping = JarMapping()
+ jarMapping.loadMappings(mapping.path, reverse, false, null, null)
+
+ val inheritance = JointProvider()
+ jarMapping.setFallbackInheritanceProvider(inheritance)
+
+ // Equivalent of --live with server jar on classpath.
+ val serverJar = Jar.init(server)
+ inheritance.add(JarProvider(serverJar))
+
+ val inputJar = Jar.init(input)
+ inheritance.add(JarProvider(inputJar))
+
+ // Remap reflective access.
+ val preprocessor = RemapperProcessor(null, jarMapping, null)
+
+ val remapper = JarRemapper(preprocessor, jarMapping, null)
+ remapper.remapJar(inputJar, output, emptySet())
+
+ serverJar.close()
+ inputJar.close()
+ }
+
+ @Internal
+ internal fun getMavenLocal(): Property {
+ return mavenLocal
+ }
+
+}
diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt
new file mode 100644
index 00000000..f9304be4
--- /dev/null
+++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt
@@ -0,0 +1,60 @@
+package com.github.jikoo.openinv
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.JavaPluginExtension
+import org.gradle.jvm.toolchain.JavaToolchainService
+import org.gradle.kotlin.dsl.create
+import java.nio.file.Paths
+import javax.inject.Inject
+
+abstract class SpigotSetup : Plugin {
+
+ @get:Inject
+ abstract val javaToolchainService: JavaToolchainService
+
+ override fun apply(target: Project) {
+ target.plugins.apply("java")
+
+ // Set up extension for configuring Spigot dependency.
+ val spigotExt = target.dependencies.extensions.findByType(SpigotDependencyExtension::class.java)
+ ?: target.dependencies.extensions.create(
+ "spigot",
+ SpigotDependencyExtension::class.java,
+ target.objects
+ )
+
+ val mvnLocal = target.repositories.mavenLocal()
+
+ target.afterEvaluate {
+ // Get Java requirements, defaulting to version used for compilation.
+ spigotExt.java.convention(target.extensions.getByType(JavaPluginExtension::class.java).toolchain)
+ val launcher = javaToolchainService.launcherFor(spigotExt.java.get()).get()
+
+ // Install Spigot with BuildTools.
+ target.providers.of(BuildToolsValueSource::class.java) {
+ parameters {
+ mavenLocal.set(Paths.get(mvnLocal.url).toFile())
+ workingDir.set(target.layout.buildDirectory.dir("tmp/buildtools"))
+ spigotVersion.set(spigotExt.version)
+ spigotRevision.set(spigotExt.revision)
+ ignoreCached.set(spigotExt.ignoreCached)
+ javaHome.set(launcher.metadata.installationPath)
+ javaExecutable.set(launcher.executablePath.asFile.path)
+ }
+ }.get()
+
+ // Add Spigot dependency.
+ val dependency = target.dependencies.create(
+ "org.spigotmc",
+ "spigot",
+ spigotExt.version.get(),
+ spigotExt.configuration.orNull,
+ spigotExt.classifier.orNull,
+ spigotExt.ext.orNull
+ )
+ target.dependencies.add("compileOnly", dependency)
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/kotlin/openinv-base.gradle.kts b/buildSrc/src/main/kotlin/openinv-base.gradle.kts
new file mode 100644
index 00000000..be1979a3
--- /dev/null
+++ b/buildSrc/src/main/kotlin/openinv-base.gradle.kts
@@ -0,0 +1,31 @@
+plugins {
+ `java-library`
+ id("net.ltgt.errorprone")
+}
+
+java {
+ toolchain.languageVersion = JavaLanguageVersion.of(21)
+}
+
+repositories {
+ mavenCentral()
+ maven("https://repo.papermc.io/repository/maven-public/")
+ maven("https://hub.spigotmc.org/nexus/content/groups/public/")
+}
+
+dependencies {
+ val libs = versionCatalogs.named("libs")
+ compileOnly(libs.findLibrary("annotations").orElseThrow())
+ compileOnly(libs.findLibrary("spigotapi").orElseThrow())
+ errorprone(libs.findLibrary("errorprone-core").orElseThrow())
+}
+
+tasks {
+ withType().configureEach {
+ options.release = 21
+ options.encoding = Charsets.UTF_8.name()
+ }
+ withType().configureEach {
+ options.encoding = Charsets.UTF_8.name()
+ }
+}
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
new file mode 100644
index 00000000..3321bb0e
--- /dev/null
+++ b/common/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ `openinv-base`
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ compileOnly(libs.slf4j.api)
+}
diff --git a/common/src/main/java/com/lishid/openinv/event/OpenEvents.java b/common/src/main/java/com/lishid/openinv/event/OpenEvents.java
new file mode 100644
index 00000000..626372c2
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/event/OpenEvents.java
@@ -0,0 +1,39 @@
+package com.lishid.openinv.event;
+
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.util.setting.PlayerToggle;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Construct and call events.
+ */
+public final class OpenEvents {
+
+ public static boolean saveCancelled(@NotNull Player player) {
+ return call(new PlayerSaveEvent(player));
+ }
+
+ public static boolean saveCancelled(@NotNull ISpecialInventory inventory) {
+ return call(new OpenPlayerSaveEvent((Player) inventory.getPlayer(), inventory));
+ }
+
+ public static void notifyPlayerToggle(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean state) {
+ Bukkit.getPluginManager().callEvent(new PlayerToggledEvent(toggle, uuid, state));
+ }
+
+ private static boolean call(T event) {
+ Bukkit.getPluginManager().callEvent(event);
+ return event.isCancelled();
+ }
+
+ private OpenEvents() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/internal/Accessor.java b/common/src/main/java/com/lishid/openinv/internal/Accessor.java
new file mode 100644
index 00000000..e57a4afb
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/internal/Accessor.java
@@ -0,0 +1,24 @@
+package com.lishid.openinv.internal;
+
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface Accessor {
+
+ @NotNull
+ PlayerManager getPlayerManager();
+
+ @NotNull IAnySilentContainer getAnySilentContainer();
+
+ @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player);
+
+ @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player);
+
+ @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz);
+
+ void reload(@NotNull ConfigurationSection config);
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java b/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java
new file mode 100644
index 00000000..df477629
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java
@@ -0,0 +1,106 @@
+package com.lishid.openinv.internal;
+
+import org.bukkit.block.Barrel;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.BlockState;
+import org.bukkit.block.EnderChest;
+import org.bukkit.block.ShulkerBox;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.block.data.type.Chest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Method;
+
+public abstract class AnySilentContainerBase implements IAnySilentContainer {
+
+ private static final @Nullable Method BLOCK_GET_STATE_BOOLEAN;
+
+ static {
+ @Nullable Method getState;
+ try {
+ //noinspection JavaReflectionMemberAccess
+ getState = Block.class.getMethod("getState", boolean.class);
+ } catch (NoSuchMethodException e) {
+ getState = null;
+ }
+ BLOCK_GET_STATE_BOOLEAN = getState;
+ }
+
+ private static BlockState getBlockState(Block block) {
+ // Paper: Get state without snapshotting.
+ if (BLOCK_GET_STATE_BOOLEAN != null) {
+ try {
+ return (BlockState) BLOCK_GET_STATE_BOOLEAN.invoke(block, false);
+ } catch (ReflectiveOperationException ignored) {
+ // If we encounter an issue, fall through to regular snapshotting method.
+ }
+ }
+ return block.getState();
+ }
+
+ @Override
+ public boolean isAnyContainerNeeded(@NotNull Block block) {
+ BlockState blockState = getBlockState(block);
+
+ // Barrels do not require AnyContainer.
+ if (blockState instanceof Barrel) {
+ return false;
+ }
+
+ // Enderchests require a non-occluding block on top to open.
+ if (blockState instanceof EnderChest) {
+ return block.getRelative(0, 1, 0).getType().isOccluding();
+ }
+
+ // Shulker boxes require half a block clear in the direction they open.
+ if (blockState instanceof ShulkerBox) {
+ return isShulkerBlocked(block);
+ }
+
+ if (!(blockState instanceof org.bukkit.block.Chest)) {
+ return false;
+ }
+
+ if (isChestBlocked(block)) {
+ return true;
+ }
+
+ BlockData blockData = block.getBlockData();
+ if (!(blockData instanceof Chest chest) || chest.getType() == Chest.Type.SINGLE) {
+ return false;
+ }
+
+ BlockFace relativeFace = switch (chest.getFacing()) {
+ case NORTH -> chest.getType() == Chest.Type.RIGHT ? BlockFace.WEST : BlockFace.EAST;
+ case EAST -> chest.getType() == Chest.Type.RIGHT ? BlockFace.NORTH : BlockFace.SOUTH;
+ case SOUTH -> chest.getType() == Chest.Type.RIGHT ? BlockFace.EAST : BlockFace.WEST;
+ case WEST -> chest.getType() == Chest.Type.RIGHT ? BlockFace.SOUTH : BlockFace.NORTH;
+ default -> BlockFace.SELF;
+ };
+ Block relative = block.getRelative(relativeFace);
+
+ if (relative.getType() != block.getType()) {
+ return false;
+ }
+
+ BlockData relativeData = relative.getBlockData();
+ if (!(relativeData instanceof Chest relativeChest)) {
+ return false;
+ }
+
+ if (relativeChest.getFacing() != chest.getFacing()
+ || relativeChest.getType() != (chest.getType() == Chest.Type.RIGHT ? Chest.Type.LEFT : Chest.Type.RIGHT)) {
+ return false;
+ }
+
+ return isChestBlocked(relative);
+ }
+
+ @Override
+ public boolean isAnySilentContainer(@NotNull Block block) {
+ return isAnySilentContainer(getBlockState(block));
+ }
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java b/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java
new file mode 100644
index 00000000..24f826d0
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java
@@ -0,0 +1,7 @@
+package com.lishid.openinv.internal;
+
+public interface InternalOwned {
+
+ T getOwnerHandle();
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java b/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java
new file mode 100644
index 00000000..c60c1557
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java
@@ -0,0 +1,54 @@
+/*
+ * 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.internal;
+
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface PlayerManager {
+
+ /**
+ * Loads a Player for an OfflinePlayer.
+ *
+ * This method is potentially blocking, and should not be called on the main thread.
+ *
+ * @param offline the OfflinePlayer
+ * @return the Player loaded
+ */
+ @Nullable Player loadPlayer(@NotNull OfflinePlayer offline);
+
+ /**
+ * Creates a new Player from an existing one that will function slightly better offline.
+ *
+ * @return the Player
+ */
+ @NotNull Player inject(@NotNull Player player);
+
+ /**
+ * Opens an ISpecialInventory for a Player.
+ *
+ * @param player the Player opening the ISpecialInventory
+ * @param inventory the Inventory
+ *
+ * @return the InventoryView opened
+ */
+ @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly);
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java b/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java
new file mode 100644
index 00000000..b8c5bfa6
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java
@@ -0,0 +1,4 @@
+package com.lishid.openinv.internal;
+
+public interface ViewOnly {
+}
diff --git a/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java b/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java
new file mode 100644
index 00000000..f53a3374
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java
@@ -0,0 +1,134 @@
+package com.lishid.openinv.util;
+
+import org.slf4j.Marker;
+import org.slf4j.helpers.LegacyAbstractLogger;
+import org.slf4j.helpers.MessageFormatter;
+import org.slf4j.helpers.NormalizedParameters;
+import org.slf4j.spi.LocationAwareLogger;
+
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * An adapter for wrapping a {@link java.util.logging.Logger} as a {@link org.slf4j.Logger}.
+ * Largely based on {@code JDK14LoggerAdapter}, which is not present at runtime.
+ */
+public class JulLoggerAdapter extends LegacyAbstractLogger implements LocationAwareLogger {
+
+ private final Logger wrapped;
+
+ public JulLoggerAdapter(Logger wrapped) {
+ this.wrapped = wrapped;
+ this.name = wrapped.getName();
+ }
+
+ @Override
+ protected String getFullyQualifiedCallerName() {
+ return JulLoggerAdapter.class.getName();
+ }
+
+ @Override
+ protected void handleNormalizedLoggingCall(
+ org.slf4j.event.Level slf4jLevel,
+ Marker marker,
+ String msg,
+ Object[] args,
+ Throwable thrown
+ ) {
+ Level level = fromSlf4jLevel(slf4jLevel);
+
+ if (wrapped.isLoggable(level)) {
+ normalizedLog(getFullyQualifiedCallerName(), level, msg, args, thrown);
+ }
+ }
+
+ private void normalizedLog(String fqcn, Level level, String msg, Object[] args, Throwable thrown) {
+ String formatted = MessageFormatter.basicArrayFormat(msg, args);
+ LogRecord logRecord = new LogRecord(level, formatted);
+ logRecord.setLoggerName(getName());
+ logRecord.setThrown(thrown);
+
+ addSource(fqcn, logRecord);
+
+ wrapped.log(logRecord);
+ }
+
+ private void addSource(String fqcn, LogRecord logRecord) {
+ // TODO stackwalker?
+ StackTraceElement[] trace = new Throwable().getStackTrace();
+ int maxElements = 12;
+ int lastIgnored = maxElements;
+ // Start from 2; 0 is above and 1 is caller of internal method.
+ for (int i = 2; i < maxElements; ++i) {
+ if (isIgnored(trace[i].getClassName(), fqcn)) {
+ lastIgnored = i;
+ }
+ }
+
+ if (lastIgnored < maxElements - 1) {
+ StackTraceElement caller = trace[lastIgnored + 1];
+ logRecord.setSourceClassName(caller.getClassName());
+ logRecord.setSourceMethodName(caller.getMethodName());
+ }
+ }
+
+ private boolean isIgnored(String className, String fqcn) {
+ if (className.equals(fqcn)) {
+ return true;
+ }
+ // Ignore slf4j classes - they shouldn't be the source.
+ if (className.startsWith("org.slf4j.")) {
+ return true;
+ }
+ return className.equals(getFullyQualifiedCallerName());
+ }
+
+ @Override
+ public void log(Marker marker, String callerFqn, int levelInt, String msg, Object[] args, Throwable thrown) {
+ Level level = fromSlf4jLevel(org.slf4j.event.Level.intToLevel(levelInt));
+
+ if (!wrapped.isLoggable(level)) {
+ return;
+ }
+
+ NormalizedParameters params = NormalizedParameters.normalize(msg, args, thrown);
+ normalizedLog(callerFqn, level, params.getMessage(), params.getArguments(), params.getThrowable());
+ }
+
+ private Level fromSlf4jLevel(org.slf4j.event.Level level) {
+ return switch (level) {
+ case TRACE -> Level.FINEST;
+ case DEBUG -> Level.FINE;
+ case INFO -> Level.INFO;
+ case WARN -> Level.WARNING;
+ case ERROR -> Level.SEVERE;
+ };
+ }
+
+ @Override
+ public boolean isTraceEnabled() {
+ return wrapped.isLoggable(Level.FINEST);
+ }
+
+ @Override
+ public boolean isDebugEnabled() {
+ return wrapped.isLoggable(Level.FINE);
+ }
+
+ @Override
+ public boolean isInfoEnabled() {
+ return wrapped.isLoggable(Level.INFO);
+ }
+
+ @Override
+ public boolean isWarnEnabled() {
+ return wrapped.isLoggable(Level.WARNING);
+ }
+
+ @Override
+ public boolean isErrorEnabled() {
+ return wrapped.isLoggable(Level.SEVERE);
+ }
+
+}
diff --git a/common/src/main/java/com/lishid/openinv/util/Permissions.java b/common/src/main/java/com/lishid/openinv/util/Permissions.java
new file mode 100644
index 00000000..911dcb5e
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/util/Permissions.java
@@ -0,0 +1,73 @@
+/*
+ * 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.util;
+
+import org.bukkit.permissions.Permissible;
+import org.jetbrains.annotations.NotNull;
+
+public enum Permissions {
+
+ INVENTORY_OPEN_SELF("inventory.open.self"),
+ INVENTORY_OPEN_OTHER("inventory.open.other"),
+ INVENTORY_EDIT_SELF("inventory.edit.self"),
+ INVENTORY_EDIT_OTHER("inventory.edit.other"),
+ INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"),
+ INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"),
+ INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"),
+ INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"),
+ INVENTORY_SLOT_DROP("inventory.slot.drop"),
+
+ ENDERCHEST_OPEN_SELF("enderchest.open.self"),
+ ENDERCHEST_OPEN_OTHER("enderchest.open.other"),
+ ENDERCHEST_EDIT_SELF("enderchest.edit.self"),
+ ENDERCHEST_EDIT_OTHER("enderchest.edit.other"),
+
+ CLEAR_SELF("clear.self"),
+ CLEAR_OTHER("clear.other"),
+
+ ACCESS_CROSSWORLD("access.crossworld"),
+ ACCESS_OFFLINE("access.offline"),
+ ACCESS_ONLINE("access.online"),
+ ACCESS_EQUAL_EDIT("access.equal.edit"),
+ ACCESS_EQUAL_VIEW("access.equal.view"),
+ ACCESS_EQUAL_DENY("access.equal.deny"),
+
+ SPECTATE_CLICK("spectate.click"),
+
+ CONTAINER_ANY("container.any"),
+ CONTAINER_ANY_USE("container.any.use"),
+ CONTAINER_SILENT("container.silent"),
+ CONTAINER_SILENT_USE("container.silent.use"),
+ SEARCH_INVENTORY("search.inventory"),
+ SEARCH_CONTAINER("search.container");
+
+ private final String permission;
+
+ Permissions(String permission) {
+ this.permission = "openinv." + permission;
+ }
+
+ public boolean hasPermission(@NotNull Permissible permissible) {
+ return permissible.hasPermission(permission);
+ }
+
+ public boolean hasPermission(@NotNull Permissible permissible, @NotNull Permissions parent) {
+ if (permissible.hasPermission(permission)) return true;
+ if (permissible.isPermissionSet(permission) && !permissible.hasPermission(permission)) return false;
+ return permissible.hasPermission(parent.permission);
+ }
+}
diff --git a/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java b/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java
new file mode 100644
index 00000000..d22e6fcf
--- /dev/null
+++ b/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java
@@ -0,0 +1,78 @@
+/*
+ * 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 org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Field;
+
+/**
+ * A utility for making reflection easier.
+ */
+public final class ReflectionHelper {
+
+ /**
+ * Grab an {@link Object} stored in a {@link Field} of another {@code Object}.
+ *
+ *
Unmodifiable because I said so. Use your own crafting grid.
+ */
+public class ContentCraftingResult extends ContentViewOnly {
+
+ public ContentCraftingResult(@NotNull ServerPlayer holder) {
+ super(holder);
+ }
+
+ @Override
+ public ItemStack get() {
+ InventoryMenu inventoryMenu = holder.inventoryMenu;
+ return inventoryMenu.getResultSlot().getItem();
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotViewOnly(container, slot, x, y) {
+ @Override
+ public ItemStack getOrDefault() {
+ if (!ContentCrafting.isAvailable(holder)) {
+ return Placeholders.survivalOnly(holder);
+ }
+ InventoryMenu inventoryMenu = holder.inventoryMenu;
+ return inventoryMenu.getResultSlot().getItem();
+ }
+ };
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.RESULT;
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java
new file mode 100644
index 00000000..2693698d
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java
@@ -0,0 +1,118 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A slot wrapping the active menu's cursor. Unavailable when not online in a survival mode.
+ */
+public class ContentCursor implements Content {
+
+ private @NotNull ServerPlayer holder;
+
+ public ContentCursor(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public ItemStack get() {
+ return isAvailable() ? holder.containerMenu.getCarried() : ItemStack.EMPTY;
+ }
+
+ @Override
+ public ItemStack remove() {
+ ItemStack carried = holder.containerMenu.getCarried();
+ holder.containerMenu.setCarried(ItemStack.EMPTY);
+ return carried;
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ ItemStack carried = holder.containerMenu.getCarried();
+ if (!carried.isEmpty() && carried.getCount() >= amount) {
+ ItemStack value = carried.split(amount);
+ if (carried.isEmpty()) {
+ holder.containerMenu.setCarried(ItemStack.EMPTY);
+ }
+ return value;
+ }
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ if (isAvailable()) {
+ holder.containerMenu.setCarried(itemStack);
+ } else {
+ holder.drop(itemStack, false);
+ }
+ }
+
+ private boolean isAvailable() {
+ // Player must be online and not in creative - since the creative client is (semi-)authoritative,
+ // it ignores changes without extra help, and will delete the item as a result.
+ // Spectator mode is technically possible but may cause the item to be dropped if the client opens an inventory.
+ return BaseOpenPlayer.isConnected(holder.connection) && holder.gameMode.isSurvival();
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotCursor(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ // As close as possible to "not real"
+ return InventoryType.SlotType.OUTSIDE;
+ }
+
+ public class SlotCursor extends SlotPlaceholder {
+
+ private SlotCursor(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ if (!isAvailable()) {
+ return Placeholders.survivalOnly(holder);
+ }
+ ItemStack carried = holder.containerMenu.getCarried();
+ return carried.isEmpty() ? Placeholders.cursor : carried;
+ }
+
+ @Override
+ public boolean mayPickup(@NotNull Player player) {
+ return isAvailable();
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ return isAvailable();
+ }
+
+ @Override
+ public boolean hasItem() {
+ return isAvailable() && super.hasItem();
+ }
+
+ @Override
+ public boolean isFake() {
+ return true;
+ }
+
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java
new file mode 100644
index 00000000..7ed6e700
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java
@@ -0,0 +1,89 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A fake slot used to drop items. Unavailable offline.
+ */
+public class ContentDrop implements Content {
+
+ private ServerPlayer holder;
+
+ public ContentDrop(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public ItemStack get() {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public ItemStack remove() {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ holder.drop(itemStack, true);
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotDrop(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ // Behaves like dropping an item outside the screen, just by the target player.
+ return InventoryType.SlotType.OUTSIDE;
+ }
+
+ public class SlotDrop extends SlotPlaceholder {
+
+ private SlotDrop(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ return BaseOpenPlayer.isConnected(holder.connection)
+ ? Placeholders.drop
+ : Placeholders.blockedOffline;
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ return BaseOpenPlayer.isConnected(holder.connection);
+ }
+
+ @Override
+ public boolean hasItem() {
+ return false;
+ }
+
+ @Override
+ public boolean isFake() {
+ return true;
+ }
+
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java
new file mode 100644
index 00000000..9087232d
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java
@@ -0,0 +1,109 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EntityEquipment;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A slot for equipment that displays placeholders if empty.
+ */
+public class ContentEquipment implements Content {
+
+ private EntityEquipment equipment;
+ private final ItemStack placeholder;
+ private final EquipmentSlot equipmentSlot;
+
+ public ContentEquipment(ServerPlayer holder, EquipmentSlot equipmentSlot) {
+ setHolder(holder);
+ placeholder = switch (equipmentSlot) {
+ case HEAD -> Placeholders.emptyHelmet;
+ case CHEST -> Placeholders.emptyChestplate;
+ case LEGS -> Placeholders.emptyLeggings;
+ case FEET -> Placeholders.emptyBoots;
+ default -> Placeholders.emptyOffHand;
+ };
+ this.equipmentSlot = equipmentSlot;
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.equipment = holder.getInventory().equipment;
+ }
+
+ @Override
+ public ItemStack get() {
+ return equipment.get(equipmentSlot);
+ }
+
+ @Override
+ public ItemStack remove() {
+ return equipment.set(equipmentSlot, ItemStack.EMPTY);
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ ItemStack current = get();
+ if (!current.isEmpty() && amount > 0) {
+ return current.split(amount);
+ }
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ equipment.set(equipmentSlot, itemStack);
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.ARMOR;
+ }
+
+ public class SlotEquipment extends SlotPlaceholder {
+
+ private ServerPlayer viewer;
+
+ SlotEquipment(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ ItemStack itemStack = getItem();
+ if (!itemStack.isEmpty()) {
+ return itemStack;
+ }
+ return placeholder;
+ }
+
+ public EquipmentSlot getEquipmentSlot() {
+ return equipmentSlot;
+ }
+
+ public void onlyEquipmentFor(ServerPlayer viewer) {
+ this.viewer = viewer;
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ if (viewer == null) {
+ return true;
+ }
+
+ return equipmentSlot == EquipmentSlot.OFFHAND || viewer.getEquipmentSlotForItem(itemStack) == equipmentSlot;
+ }
+
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java
new file mode 100644
index 00000000..5748c51a
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java
@@ -0,0 +1,58 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.ContainerHelper;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+
+import java.util.List;
+
+/**
+ * A normal slot backed by an item list.
+ */
+public abstract class ContentList implements Content {
+
+ private final int index;
+ private final InventoryType.SlotType slotType;
+ protected List items;
+
+ public ContentList(ServerPlayer holder, int index, InventoryType.SlotType slotType) {
+ this.index = index;
+ this.slotType = slotType;
+ setHolder(holder);
+ }
+
+ @Override
+ public ItemStack get() {
+ return items.get(index);
+ }
+
+ @Override
+ public ItemStack remove() {
+ ItemStack removed = items.remove(index);
+ return removed == null || removed.isEmpty() ? ItemStack.EMPTY : removed;
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ return ContainerHelper.removeItem(items, index, amount);
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ items.set(index, itemStack);
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new Slot(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return slotType;
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java
new file mode 100644
index 00000000..ba1db443
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java
@@ -0,0 +1,53 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.InventoryMenu;
+import net.minecraft.world.inventory.Slot;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A slot for equipment that updates held items if necessary.
+ */
+public class ContentOffHand extends ContentEquipment {
+
+ private ServerPlayer holder;
+
+ public ContentOffHand(ServerPlayer holder) {
+ super(holder, EquipmentSlot.OFFHAND);
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ super.setHolder(holder);
+ this.holder = holder;
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.QUICKBAR;
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y) {
+ @Override
+ public void setChanged() {
+ if (BaseOpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) {
+ holder.connection.send(
+ new ClientboundContainerSetSlotPacket(
+ holder.inventoryMenu.containerId,
+ holder.inventoryMenu.incrementStateId(),
+ InventoryMenu.SHIELD_SLOT,
+ holder.getOffhandItem()
+ ));
+ }
+ }
+ };
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java
new file mode 100644
index 00000000..319ef0e5
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java
@@ -0,0 +1,56 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A view-only slot that can't be interacted with.
+ */
+public class ContentViewOnly implements Content {
+
+ protected @NotNull ServerPlayer holder;
+
+ public ContentViewOnly(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.holder = holder;
+ }
+
+ @Override
+ public ItemStack get() {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public ItemStack remove() {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ this.holder.drop(itemStack, false);
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotViewOnly(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.OUTSIDE;
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java
new file mode 100644
index 00000000..7e7a79ad
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java
@@ -0,0 +1,20 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import net.minecraft.world.Container;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+
+/**
+ * An implementation of a slot as used by a menu that may have fake placeholder items.
+ *
+ *
Used to prevent plugins (particularly sorting plugins) from adding placeholders to inventories.
+ */
+public abstract class SlotPlaceholder extends Slot {
+
+ public SlotPlaceholder(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ public abstract ItemStack getOrDefault();
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java
new file mode 100644
index 00000000..3fe82fd9
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java
@@ -0,0 +1,151 @@
+package com.lishid.openinv.internal.common.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+
+/**
+ * A view-only {@link Slot}. "Blank" by default, but can wrap another slot to display its content.
+ */
+public class SlotViewOnly extends SlotPlaceholder {
+
+ public static @NotNull SlotViewOnly wrap(@NotNull Slot wrapped) {
+ SlotViewOnly wrapper;
+ if (wrapped instanceof SlotPlaceholder placeholder) {
+ wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) {
+ @Override
+ public ItemStack getOrDefault() {
+ return placeholder.getOrDefault();
+ }
+ };
+ } else {
+ wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) {
+ @Override
+ public ItemStack getOrDefault() {
+ return wrapped.getItem();
+ }
+ };
+ }
+ wrapper.index = wrapped.index;
+ return wrapper;
+ }
+
+ public SlotViewOnly(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ return Placeholders.notSlot;
+ }
+
+ @Override
+ public void onQuickCraft(@NotNull ItemStack itemStack1, @NotNull ItemStack itemStack2) {
+ }
+
+ @Override
+ public void onTake(@NotNull Player player, @NotNull ItemStack itemStack) {
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ return false;
+ }
+
+ @Override
+ public @NotNull ItemStack getItem() {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public boolean hasItem() {
+ return false;
+ }
+
+ @Override
+ public void setByPlayer(@NotNull ItemStack newStack) {
+ }
+
+ @Override
+ public void setByPlayer(@NotNull ItemStack newStack, @NotNull ItemStack oldStack) {
+ }
+
+ @Override
+ public void set(@NotNull ItemStack itemStack) {
+ }
+
+ @Override
+ public void setChanged() {
+ }
+
+ @Override
+ public int getMaxStackSize() {
+ return 0;
+ }
+
+ @Override
+ public int getMaxStackSize(@NotNull ItemStack itemStack) {
+ return 0;
+ }
+
+ @Override
+ public @NotNull ItemStack remove(int amount) {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public boolean mayPickup(@NotNull Player player) {
+ return false;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public @NotNull Optional tryRemove(int var0, int var1, @NotNull Player player) {
+ return Optional.empty();
+ }
+
+ @Override
+ public @NotNull ItemStack safeTake(int var0, int var1, @NotNull Player player) {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public @NotNull ItemStack safeInsert(@NotNull ItemStack itemStack) {
+ return itemStack;
+ }
+
+ @Override
+ public @NotNull ItemStack safeInsert(@NotNull ItemStack itemStack, int amount) {
+ return itemStack;
+ }
+
+ @Override
+ public boolean allowModification(@NotNull Player player) {
+ return false;
+ }
+
+ @Override
+ public int getContainerSlot() {
+ return this.slot;
+ }
+
+ @Override
+ public boolean isHighlightable() {
+ return false;
+ }
+
+ @Override
+ public boolean isFake() {
+ return true;
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java
new file mode 100644
index 00000000..c859e7d6
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java
@@ -0,0 +1,40 @@
+package com.lishid.openinv.internal.common.container.slot.placeholder;
+
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.TagParser;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.CustomModelData;
+import net.minecraft.world.item.component.DyedItemColor;
+import net.minecraft.world.item.component.TooltipDisplay;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+
+public class PlaceholderLoader extends PlaceholderLoaderBase {
+
+ private static final CustomModelData DEFAULT_CUSTOM_MODEL_DATA = new CustomModelData(List.of(), List.of(), List.of("openinv:custom"), List.of());
+ private static final TooltipDisplay HIDE_TOOLTIP = new TooltipDisplay(true, new LinkedHashSet<>());
+
+ @Override
+ protected @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception {
+ return TagParser.parseCompoundFully(itemText);
+ }
+
+ @Override
+ protected void addModelData(@NotNull ItemStack itemStack) {
+ itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
+ }
+
+ @Override
+ protected void hideTooltip(@NotNull ItemStack itemStack) {
+ itemStack.set(DataComponents.TOOLTIP_DISPLAY, HIDE_TOOLTIP);
+ }
+
+ @Override
+ protected DyedItemColor getDye(int rgb) {
+ return new DyedItemColor(rgb);
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java
new file mode 100644
index 00000000..4de686d0
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java
@@ -0,0 +1,180 @@
+package com.lishid.openinv.internal.common.container.slot.placeholder;
+
+import com.mojang.serialization.DataResult;
+import net.minecraft.core.Registry;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.network.chat.Component;
+import net.minecraft.world.item.DyeColor;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.component.DyedItemColor;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.ItemLike;
+import net.minecraft.world.level.block.entity.BannerPattern;
+import net.minecraft.world.level.block.entity.BannerPatternLayers;
+import net.minecraft.world.level.block.entity.BannerPatterns;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public abstract class PlaceholderLoaderBase {
+
+ public void load(@Nullable ConfigurationSection section) throws Exception {
+ Placeholders.craftingOutput = parse(section, "crafting-output", defaultCraftingOutput());
+ Placeholders.cursor = parse(section, "cursor", defaultCursor());
+ Placeholders.drop = parse(section, "drop", defaultDrop());
+ Placeholders.emptyHelmet = parse(section, "empty-helmet", getEmptyArmor(Items.LEATHER_HELMET));
+ Placeholders.emptyChestplate = parse(section, "empty-chestplate", getEmptyArmor(Items.LEATHER_CHESTPLATE));
+ Placeholders.emptyLeggings = parse(section, "empty-leggings", getEmptyArmor(Items.LEATHER_LEGGINGS));
+ Placeholders.emptyBoots = parse(section, "empty-boots", getEmptyArmor(Items.LEATHER_BOOTS));
+ Placeholders.emptyOffHand = parse(section, "empty-off-hand", defaultShield());
+ Placeholders.notSlot = parse(section, "not-a-slot", defaultNotSlot());
+ Placeholders.blockedOffline = parse(section, "blocked.offline", defaultBlockedOffline());
+
+ for (GameType type : GameType.values()) {
+ // Barrier: "Not available - Creative" etc.
+ ItemStack typeItem = new ItemStack(Items.BARRIER);
+ typeItem.set(
+ DataComponents.ITEM_NAME,
+ Component.translatable("options.narrator.notavailable").append(" - ").append(type.getShortDisplayName())
+ );
+ Placeholders.BLOCKED_GAME_TYPE.put(type, typeItem);
+ }
+
+ Placeholders.BLOCKED_GAME_TYPE.put(GameType.CREATIVE, parse(section, "blocked.creative", Placeholders.BLOCKED_GAME_TYPE.get(GameType.CREATIVE)));
+ Placeholders.BLOCKED_GAME_TYPE.put(GameType.SPECTATOR, parse(section, "blocked.spectator", Placeholders.BLOCKED_GAME_TYPE.get(GameType.SPECTATOR)));
+ }
+
+ protected @NotNull ItemStack parse(
+ @Nullable ConfigurationSection section,
+ @NotNull String path,
+ @NotNull ItemStack defaultStack
+ ) throws Exception {
+ if (section == null) {
+ return defaultStack;
+ }
+
+ String itemText = section.getString(path);
+
+ if (itemText == null) {
+ return defaultStack;
+ }
+
+ CompoundTag compoundTag = parseTag(itemText);
+ DataResult parsed = ItemStack.CODEC.parse(CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE), compoundTag);
+ ItemStack itemStack;
+ try {
+ itemStack = parsed.getOrThrow();
+ } catch (Exception e) {
+ itemStack = null;
+ }
+ return itemStack == null ? defaultStack : itemStack;
+ }
+
+ protected abstract @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception;
+
+ protected abstract void addModelData(@NotNull ItemStack itemStack);
+
+ protected abstract void hideTooltip(@NotNull ItemStack itemStack);
+
+ protected abstract DyedItemColor getDye(int rgb);
+
+ protected @NotNull ItemStack defaultCraftingOutput() {
+ // Crafting table: "Crafting"
+ ItemStack itemStack = new ItemStack(Items.CRAFTING_TABLE);
+ itemStack.set(DataComponents.ITEM_NAME, Component.translatable("container.crafting"));
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack defaultCursor() {
+ // Cursor-like banner with no tooltip
+ ItemStack itemStack = new ItemStack(Items.WHITE_BANNER);
+ RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
+ Registry bannerPatterns = minecraftRegistry.lookupOrThrow(Registries.BANNER_PATTERN);
+ BannerPattern halfDiagBottomRight = bannerPatterns.getOrThrow(BannerPatterns.DIAGONAL_RIGHT).value();
+ BannerPattern downRight = bannerPatterns.getOrThrow(BannerPatterns.STRIPE_DOWNRIGHT).value();
+ BannerPattern border = bannerPatterns.getOrThrow(BannerPatterns.BORDER).value();
+ itemStack.set(DataComponents.BANNER_PATTERNS,
+ new BannerPatternLayers(List.of(
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfDiagBottomRight), DyeColor.GRAY),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(downRight), DyeColor.WHITE),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(border), DyeColor.GRAY)
+ ))
+ );
+ addModelData(itemStack);
+ hideTooltip(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack defaultDrop() {
+ // Dropper: "Drop Selected Item"
+ ItemStack itemStack = new ItemStack(Items.DROPPER);
+ // Note: translatable component, not keybind component! We want the text identifying the keybind, not the key.
+ itemStack.set(DataComponents.ITEM_NAME, Component.translatable("key.drop"));
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack getEmptyArmor(@NotNull ItemLike item) {
+ // Inventory-background-grey-ish leather armor with no tooltip
+ ItemStack itemStack = new ItemStack(item);
+ DyedItemColor color = getDye(0xC8C8C8);
+ itemStack.set(DataComponents.DYED_COLOR, color);
+ hideTooltip(itemStack);
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack defaultShield() {
+ // Shield with "missing texture" pattern, magenta and black squares.
+ ItemStack itemStack = new ItemStack(Items.SHIELD);
+ itemStack.set(DataComponents.BASE_COLOR, DyeColor.MAGENTA);
+ RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
+ Registry bannerPatterns = minecraftRegistry.lookupOrThrow(Registries.BANNER_PATTERN);
+ BannerPattern halfLeft = bannerPatterns.getOrThrow(BannerPatterns.HALF_VERTICAL).value();
+ BannerPattern topLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_LEFT).value();
+ BannerPattern topRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_RIGHT).value();
+ BannerPattern bottomLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_LEFT).value();
+ BannerPattern bottomRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_RIGHT).value();
+ itemStack.set(DataComponents.BANNER_PATTERNS,
+ new BannerPatternLayers(List.of(
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfLeft), DyeColor.BLACK),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topLeft), DyeColor.MAGENTA),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomLeft), DyeColor.MAGENTA),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topRight), DyeColor.BLACK),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomRight), DyeColor.BLACK)
+ ))
+ );
+ hideTooltip(itemStack);
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack defaultNotSlot() {
+ // White pane with no tooltip
+ ItemStack itemStack = new ItemStack(Items.WHITE_STAINED_GLASS_PANE);
+ hideTooltip(itemStack);
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+ protected @NotNull ItemStack defaultBlockedOffline() {
+ // Barrier: "Not available - Offline"
+ ItemStack itemStack = new ItemStack(Items.BARRIER);
+ itemStack.set(DataComponents.ITEM_NAME,
+ Component.translatable("options.narrator.notavailable")
+ .append(Component.literal(" - "))
+ .append(Component.translatable("gui.socialInteractions.status_offline"))
+ );
+ return itemStack;
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java
new file mode 100644
index 00000000..b9fcd7cb
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java
@@ -0,0 +1,37 @@
+package com.lishid.openinv.internal.common.container.slot.placeholder;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.GameType;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.EnumMap;
+
+public final class Placeholders {
+
+ static final @NotNull EnumMap BLOCKED_GAME_TYPE = new EnumMap<>(GameType.class);
+ public static @NotNull ItemStack craftingOutput = ItemStack.EMPTY;
+ public static @NotNull ItemStack cursor = ItemStack.EMPTY;
+ public static @NotNull ItemStack drop = ItemStack.EMPTY;
+ public static @NotNull ItemStack emptyHelmet = ItemStack.EMPTY;
+ public static @NotNull ItemStack emptyChestplate = ItemStack.EMPTY;
+ public static @NotNull ItemStack emptyLeggings = ItemStack.EMPTY;
+ public static @NotNull ItemStack emptyBoots = ItemStack.EMPTY;
+ public static @NotNull ItemStack emptyOffHand = ItemStack.EMPTY;
+ public static @NotNull ItemStack notSlot = ItemStack.EMPTY;
+ public static @NotNull ItemStack blockedOffline = ItemStack.EMPTY;
+
+ public static ItemStack survivalOnly(@NotNull ServerPlayer serverPlayer) {
+ if (!BaseOpenPlayer.isConnected(serverPlayer.connection)) {
+ return blockedOffline;
+ }
+
+ return BLOCKED_GAME_TYPE.getOrDefault(serverPlayer.gameMode.getGameModeForPlayer(), ItemStack.EMPTY);
+ }
+
+ private Placeholders() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java
new file mode 100644
index 00000000..77e9fb50
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java
@@ -0,0 +1,212 @@
+package com.lishid.openinv.internal.common.player;
+
+import com.lishid.openinv.event.OpenEvents;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtIo;
+import net.minecraft.nbt.NumericTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.world.level.storage.PlayerDataStorage;
+import net.minecraft.world.level.storage.ValueOutput;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+public abstract class BaseOpenPlayer extends CraftPlayer {
+
+ /**
+ * List of tags to always reset when saving. These are items that do not get written
+ * if unset or empty, resulting in older values not being clobbered appropriately.
+ *
+ * @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput, boolean, boolean, boolean)
+ * @see net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(ValueOutput)
+ * @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(ValueOutput)
+ * @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(ValueOutput)
+ */
+ @Unmodifiable
+ protected static final Set RESET_TAGS = Set.of(
+ // Entity#saveWithoutId(CompoundTag)
+ "CustomName",
+ "CustomNameVisible",
+ "Silent",
+ "NoGravity",
+ "Glowing",
+ "TicksFrozen",
+ "HasVisualFire",
+ "Tags",
+ "data",
+ "Passengers",
+ // ServerPlayer#addAdditionalSaveData(CompoundTag)
+ // Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
+ "warden_spawn_tracker", // No longer needed as of 1.21.11
+ "entered_nether_pos", // Replaces enteredNetherPosition as of 1.21.6
+ "enteredNetherPosition",
+ "respawn", // Replaces SpawnXyz fields as of 1.21.6
+ "SpawnX",
+ "SpawnY",
+ "SpawnZ",
+ "SpawnForced",
+ "SpawnAngle",
+ "SpawnDimension",
+ "raid_omen_position",
+ "ender_pearls",
+ "ShoulderEntityLeft",
+ "ShoulderEntityRight",
+ // Player#addAdditionalSaveData(CompoundTag)
+ "LastDeathLocation",
+ "current_explosion_impact_pos",
+ // LivingEntity#addAdditionalSaveData(CompoundTag)
+ "active_effects",
+ "sleeping_pos", // Replaces SleepingXyz fields as of 1.21.6
+ "SleepingX",
+ "SleepingY",
+ "SleepingZ",
+ "Brain",
+ "last_hurt_by_player",
+ "last_hurt_by_player_memory_time",
+ "last_hurt_by_mob",
+ "ticks_since_last_hurt_by_mob",
+ "equipment",
+ "locator_bar_icon"
+ );
+
+ private final PlayerManager manager;
+
+ protected BaseOpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
+ super(server, entity);
+ this.manager = manager;
+ }
+
+ @Override
+ public void loadData() {
+ manager.loadData(server.getServer(), getHandle());
+ }
+
+ @Override
+ public void saveData() {
+ if (OpenEvents.saveCancelled(this)) {
+ return;
+ }
+
+ trySave(this.getHandle());
+ }
+
+ protected abstract void trySave(ServerPlayer player);
+
+ protected void saveSafe(
+ @NotNull ServerPlayer player,
+ @Nullable CompoundTag oldData,
+ @NotNull CompoundTag playerData,
+ @NotNull PlayerDataStorage worldNbtStorage
+ ) throws IOException {
+ // Revert certain special data values when offline.
+ revertSpecialValues(playerData, oldData);
+
+ Path playerDataDir = worldNbtStorage.getPlayerDir().toPath();
+ Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
+ NbtIo.writeCompressed(playerData, tempFile);
+ Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
+ Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
+ safeReplaceFile(dataFile, tempFile, backupFile);
+ }
+
+ protected void safeReplaceFile(
+ @NotNull Path dataFile,
+ @NotNull Path tempFile,
+ @NotNull Path backupFile
+ ) {
+ net.minecraft.util.Util.safeReplaceFile(dataFile, tempFile, backupFile);
+ }
+
+ @Contract("null -> new")
+ protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
+ if (oldData == null) {
+ return new CompoundTag();
+ }
+
+ // Copy old data. This is a deep clone, so operating on it should be safe.
+ oldData = oldData.copy();
+
+ // Remove vanilla/server data that is not written every time.
+ oldData.keySet().removeIf(
+ key -> RESET_TAGS.contains(key)
+ || key.startsWith("Bukkit")
+ || (key.startsWith("Paper") && key.length() > 5)
+ );
+
+ return oldData;
+ }
+
+ protected void revertSpecialValues(@NotNull CompoundTag newData, @Nullable CompoundTag oldData) {
+ if (oldData == null) {
+ return;
+ }
+
+ // Revert automatic updates to play timestamps.
+ copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
+ copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
+ copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class);
+ }
+
+ private void copyValue(
+ @NotNull CompoundTag source,
+ @NotNull CompoundTag target,
+ @NotNull String container,
+ @NotNull String key,
+ @SuppressWarnings("SameParameterValue") @NotNull Class tagType
+ ) {
+ CompoundTag oldContainer = getTag(source, container, CompoundTag.class);
+ CompoundTag newContainer = getTag(target, container, CompoundTag.class);
+
+ // New container being null means the server implementation doesn't store this data.
+ if (newContainer == null) {
+ return;
+ }
+
+ // If old tag exists, copy it to new location, removing otherwise.
+ setTag(newContainer, key, getTag(oldContainer, key, tagType));
+ }
+
+ private @Nullable T getTag(
+ @Nullable CompoundTag container,
+ @NotNull String key,
+ @NotNull Class dataType
+ ) {
+ if (container == null) {
+ return null;
+ }
+ Tag value = container.get(key);
+ if (value == null || !dataType.isAssignableFrom(value.getClass())) {
+ return null;
+ }
+ return dataType.cast(value);
+ }
+
+ private void setTag(
+ @NotNull CompoundTag container,
+ @NotNull String key,
+ @Nullable T data
+ ) {
+ if (data == null) {
+ remove(container, key);
+ } else {
+ container.put(key, data);
+ }
+ }
+
+ protected abstract void remove(@NotNull CompoundTag tag, @NotNull String key);
+
+ public static boolean isConnected(@Nullable ServerGamePacketListenerImpl connection) {
+ return connection != null && !connection.isDisconnected();
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java
new file mode 100644
index 00000000..80f87129
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java
@@ -0,0 +1,50 @@
+package com.lishid.openinv.internal.common.player;
+
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.world.level.storage.PlayerDataStorage;
+import net.minecraft.world.level.storage.TagValueOutput;
+import net.minecraft.world.level.storage.ValueOutput;
+import org.bukkit.craftbukkit.CraftServer;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+public class OpenPlayer extends BaseOpenPlayer {
+
+ protected OpenPlayer(
+ CraftServer server,
+ ServerPlayer entity,
+ PlayerManager manager
+ ) {
+ super(server, entity, manager);
+ }
+
+ @Override
+ protected void trySave(ServerPlayer player) {
+ Logger logger = LogUtils.getLogger();
+ // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
+ PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo;
+
+ CompoundTag oldData = isOnline()
+ ? null
+ : worldNbtStorage.load(player.nameAndId()).orElse(null);
+ CompoundTag playerData = getWritableTag(oldData);
+
+ ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData);
+ player.saveWithoutId(valueOutput);
+
+ saveSafe(player, oldData, playerData, worldNbtStorage);
+ } catch (Exception e) {
+ LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
+ }
+ }
+
+ @Override
+ protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
+ tag.remove(key);
+ }
+
+}
diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java
new file mode 100644
index 00000000..b43348a5
--- /dev/null
+++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java
@@ -0,0 +1,287 @@
+package com.lishid.openinv.internal.common.player;
+
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.internal.common.container.BaseOpenInventory;
+import com.lishid.openinv.internal.common.container.OpenEnderChest;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import com.lishid.openinv.util.JulLoggerAdapter;
+import com.mojang.authlib.GameProfile;
+import io.papermc.paper.adventure.PaperAdventure;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ParticleStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.ChatVisiblity;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.storage.LevelData;
+import net.minecraft.world.level.storage.TagValueInput;
+import net.minecraft.world.level.storage.ValueInput;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.Server;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.entity.CraftPlayer;
+import org.bukkit.craftbukkit.event.CraftEventFactory;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Field;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+public class PlayerManager implements com.lishid.openinv.internal.PlayerManager {
+
+ protected final @NotNull Logger logger;
+ protected @Nullable Field bukkitEntity;
+
+ public PlayerManager(@NotNull Logger logger) {
+ this.logger = logger;
+ try {
+ bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
+ } catch (NoSuchFieldException e) {
+ logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!");
+ logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
+ bukkitEntity = null;
+ }
+ }
+
+ public static @NotNull ServerPlayer getHandle(final Player player) {
+ if (player instanceof CraftPlayer craftPlayer) {
+ return craftPlayer.getHandle();
+ }
+
+ Server server = player.getServer();
+ ServerPlayer nmsPlayer = null;
+
+ if (server instanceof CraftServer craftServer) {
+ nmsPlayer = craftServer.getHandle().getPlayer(player.getUniqueId());
+ }
+
+ if (nmsPlayer == null) {
+ // Could use reflection to examine fields, but it's honestly not worth the bother.
+ throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName());
+ }
+
+ return nmsPlayer;
+ }
+
+ @Override
+ public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
+ if (!(Bukkit.getServer() instanceof CraftServer craftServer)) {
+ return null;
+ }
+
+ MinecraftServer server = craftServer.getServer();
+ ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
+
+ if (worldServer == null) {
+ return null;
+ }
+
+ // Create a new ServerPlayer.
+ ServerPlayer entity = createNewPlayer(server, worldServer, offline);
+
+ // Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak.
+ entity.getAdvancements().stopListening();
+
+ // Try to load the player's data.
+ if (loadData(server, entity)) {
+ // If data is loaded successfully, return the Bukkit entity.
+ return entity.getBukkitEntity();
+ }
+
+ return null;
+ }
+
+ protected @NotNull ServerPlayer createNewPlayer(
+ @NotNull MinecraftServer server,
+ @NotNull ServerLevel worldServer,
+ @NotNull final OfflinePlayer offline
+ ) {
+ // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
+ // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
+ GameProfile profile = new GameProfile(offline.getUniqueId(),
+ offline.getName() != null ? offline.getName() : offline.getUniqueId().toString()
+ );
+
+ ClientInformation dummyInfo = new ClientInformation(
+ "en_us",
+ 1, // Reduce distance just in case.
+ ChatVisiblity.HIDDEN, // Don't accept chat.
+ false,
+ ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
+ ServerPlayer.DEFAULT_MAIN_HAND,
+ true,
+ false, // Don't list in player list (not that this player is in the list anyway).
+ ParticleStatus.MINIMAL
+ );
+
+ ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
+
+ try {
+ injectPlayer(server, entity);
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ }
+
+ return entity;
+ }
+
+ protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ // See CraftPlayer#loadData
+
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
+ CompoundTag loadedData = server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null);
+
+ if (loadedData == null) {
+ // Exceptions with loading are logged.
+ return false;
+ }
+
+ ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData);
+
+ // Read basic data into the player.
+ player.load(valueInput);
+
+ // World is not loaded by ServerPlayer#load(CompoundTag) on Paper.
+ parseWorld(server, player, valueInput);
+ }
+
+ return true;
+ }
+
+ protected void parseWorld(
+ @NotNull MinecraftServer server,
+ @NotNull ServerPlayer player,
+ @NotNull ValueInput loadedData
+ ) {
+ // See PlayerList#placeNewPlayer
+ World bukkitWorld;
+ Optional msbs = loadedData.getLong("WorldUUIDMost");
+ Optional lsbs = loadedData.getLong("WorldUUIDLeast");
+ if (msbs.isPresent() && lsbs.isPresent()) {
+ // Modern Bukkit world.
+ bukkitWorld = Bukkit.getServer().getWorld(new UUID(msbs.get(), lsbs.get()));
+ } else {
+ bukkitWorld = loadedData.getString("world").map(Bukkit::getWorld).orElse(null);
+ }
+ if (bukkitWorld == null) {
+ spawnInDefaultWorld(server, player);
+ return;
+ }
+ player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
+ }
+
+ protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ ServerLevel level = server.getLevel(Level.OVERWORLD);
+ if (level != null) {
+ // Adjust player to default spawn (in keeping with Paper handling) when world not found.
+ LevelData.RespawnData respawnData = level.levelData.getRespawnData();
+ player.snapTo(player.adjustSpawnLocation(level, respawnData.pos()).getBottomCenter(), respawnData.yaw(), 0.0F);
+ player.spawnIn(level);
+ } else {
+ logger.warning("Tried to load player with invalid world when no fallback was available!");
+ }
+ }
+
+ protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+
+ bukkitEntity.set(player, new OpenPlayer(server.server, player, this));
+ }
+
+ @Override
+ public @NotNull Player inject(@NotNull Player player) {
+ try {
+ ServerPlayer nmsPlayer = getHandle(player);
+ if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) {
+ return openPlayer;
+ }
+ MinecraftServer server = nmsPlayer.level().getServer();
+ injectPlayer(server, nmsPlayer);
+ return nmsPlayer.getBukkitEntity();
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ return player;
+ }
+ }
+
+ @Override
+ public @Nullable InventoryView openInventory(
+ @NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory,
+ boolean viewOnly
+ ) {
+ ServerPlayer player = getHandle(bukkitPlayer);
+
+ if (!BaseOpenPlayer.isConnected(player.connection)) {
+ return null;
+ }
+
+ // See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
+ OpenChestMenu> menu;
+ Component title;
+ if (inventory instanceof BaseOpenInventory playerInv) {
+ menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly);
+ title = playerInv.getTitle(player, menu);
+ } else if (inventory instanceof OpenEnderChest enderChest) {
+ menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly);
+ title = enderChest.getTitle(menu);
+ } else {
+ return null;
+ }
+
+ // Should never happen, player is a ServerPlayer with an active connection.
+ if (menu == null) {
+ return null;
+ }
+
+ // Set up title. Title can only be set once for a menu, and is set during the open process.
+ // Further title changes are a hack where the client is sent a "new" inventory with the same ID,
+ // resulting in a title change but no other state modifications (like cursor position).
+ menu.setTitle(title);
+
+ var pair = CraftEventFactory.callInventoryOpenEventWithTitle(player, menu);
+ AbstractContainerMenu opened = pair.getSecond();
+
+ // Menu is null if event is cancelled.
+ if (opened == null) {
+ return null;
+ }
+
+ var newTitle = pair.getFirst();
+ if (newTitle != null) {
+ title = PaperAdventure.asVanilla(newTitle);
+ }
+
+ player.containerMenu = opened;
+ player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), title));
+ player.initMenu(opened);
+
+ return opened.getBukkitView();
+ }
+
+}
diff --git a/internal/paper1_21_1/build.gradle.kts b/internal/paper1_21_1/build.gradle.kts
new file mode 100644
index 00000000..84ba9a0d
--- /dev/null
+++ b/internal/paper1_21_1/build.gradle.kts
@@ -0,0 +1,30 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+ implementation(project(":openinvadapterpaper1_21_8"))
+ implementation(project(":openinvadapterpaper1_21_5"))
+ implementation(project(":openinvadapterpaper1_21_4"))
+ implementation(project(":openinvadapterpaper1_21_3"))
+
+ paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java
new file mode 100644
index 00000000..100bf31f
--- /dev/null
+++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java
@@ -0,0 +1,80 @@
+package com.lishid.openinv.internal.paper1_21_1;
+
+import com.lishid.openinv.internal.Accessor;
+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.internal.common.container.AnySilentContainer;
+import com.lishid.openinv.internal.paper1_21_1.container.OpenInventory;
+import com.lishid.openinv.internal.paper1_21_1.container.slot.placeholder.PlaceholderLoader;
+import com.lishid.openinv.internal.paper1_21_1.player.PlayerManager;
+import com.lishid.openinv.internal.paper1_21_4.container.OpenEnderChest;
+import com.lishid.openinv.util.lang.LanguageManager;
+import net.minecraft.world.Container;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.craftbukkit.inventory.CraftInventory;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class InternalAccessor implements Accessor {
+
+ private final @NotNull Logger logger;
+ private final @NotNull PlayerManager manager;
+ private final @NotNull AnySilentContainer anySilentContainer;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ this.logger = logger;
+ this.manager = new PlayerManager(logger);
+ this.anySilentContainer = new AnySilentContainer(logger, lang);
+ }
+
+ @Override
+ public @NotNull PlayerManager getPlayerManager() {
+ return manager;
+ }
+
+ @Override
+ public @NotNull IAnySilentContainer getAnySilentContainer() {
+ return anySilentContainer;
+ }
+
+ @Override
+ public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
+ return new OpenInventory(player);
+ }
+
+ @Override
+ public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) {
+ return new OpenEnderChest(player);
+ }
+
+ @Override
+ public @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz) {
+ if (!(bukkitInventory instanceof CraftInventory craftInventory)) {
+ return null;
+ }
+ Container container = craftInventory.getInventory();
+ if (clazz.isInstance(container)) {
+ return clazz.cast(container);
+ }
+ return null;
+ }
+
+ @Override
+ public void reload(@NotNull ConfigurationSection config) {
+ ConfigurationSection placeholders = config.getConfigurationSection("placeholders");
+ try {
+ // Reset placeholders to defaults and try to load configuration.
+ new PlaceholderLoader().load(placeholders);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e);
+ }
+ }
+
+}
diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java
new file mode 100644
index 00000000..1368d89a
--- /dev/null
+++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java
@@ -0,0 +1,20 @@
+package com.lishid.openinv.internal.paper1_21_1.container;
+
+import com.lishid.openinv.internal.common.container.slot.Content;
+import com.lishid.openinv.internal.paper1_21_1.container.slot.ContentCraftingResult;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+public class OpenInventory extends com.lishid.openinv.internal.paper1_21_4.container.OpenInventory {
+
+ public OpenInventory(@NotNull Player bukkitPlayer) {
+ super(bukkitPlayer);
+ }
+
+ @Override
+ protected Content getCraftingResult(@NotNull ServerPlayer serverPlayer) {
+ return new ContentCraftingResult(serverPlayer);
+ }
+
+}
diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java
new file mode 100644
index 00000000..d0d74f74
--- /dev/null
+++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java
@@ -0,0 +1,46 @@
+package com.lishid.openinv.internal.paper1_21_1.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.ContentCrafting;
+import com.lishid.openinv.internal.common.container.slot.ContentViewOnly;
+import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.inventory.InventoryMenu;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+public class ContentCraftingResult extends ContentViewOnly {
+
+ public ContentCraftingResult(@NotNull ServerPlayer holder) {
+ super(holder);
+ }
+
+ @Override
+ public ItemStack get() {
+ InventoryMenu inventoryMenu = holder.inventoryMenu;
+ return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem();
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotViewOnly(container, slot, x, y) {
+ @Override
+ public ItemStack getOrDefault() {
+ if (!ContentCrafting.isAvailable(holder)) {
+ return Placeholders.survivalOnly(holder);
+ }
+ InventoryMenu inventoryMenu = holder.inventoryMenu;
+ return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem();
+ }
+ };
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.RESULT;
+ }
+
+}
diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java
new file mode 100644
index 00000000..0ac03f26
--- /dev/null
+++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java
@@ -0,0 +1,74 @@
+package com.lishid.openinv.internal.paper1_21_1.container.slot.placeholder;
+
+import com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder.NumericDataPlaceholderLoader;
+import net.minecraft.core.Registry;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.Registries;
+import net.minecraft.util.Unit;
+import net.minecraft.world.item.DyeColor;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.block.entity.BannerPattern;
+import net.minecraft.world.level.block.entity.BannerPatternLayers;
+import net.minecraft.world.level.block.entity.BannerPatterns;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PlaceholderLoader extends NumericDataPlaceholderLoader {
+
+ @Override
+ protected @NotNull ItemStack defaultCursor() {
+ // Cursor-like banner with no tooltip
+ ItemStack itemStack = new ItemStack(Items.WHITE_BANNER);
+ RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
+ Registry bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN);
+ BannerPattern halfDiagBottomRight = bannerPatterns.getOrThrow(BannerPatterns.DIAGONAL_RIGHT);
+ BannerPattern downRight = bannerPatterns.getOrThrow(BannerPatterns.STRIPE_DOWNRIGHT);
+ BannerPattern border = bannerPatterns.getOrThrow(BannerPatterns.BORDER);
+ itemStack.set(
+ DataComponents.BANNER_PATTERNS,
+ new BannerPatternLayers(
+ List.of(
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfDiagBottomRight), DyeColor.GRAY),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(downRight), DyeColor.WHITE),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(border), DyeColor.GRAY)
+ )
+ )
+ );
+ addModelData(itemStack);
+ itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
+ return itemStack;
+ }
+
+ @Override
+ protected @NotNull ItemStack defaultShield() {
+ // Shield with "missing texture" pattern, magenta and black squares.
+ ItemStack itemStack = new ItemStack(Items.SHIELD);
+ itemStack.set(DataComponents.BASE_COLOR, DyeColor.MAGENTA);
+ RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
+ Registry bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN);
+ BannerPattern halfLeft = bannerPatterns.getOrThrow(BannerPatterns.HALF_VERTICAL);
+ BannerPattern topLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_LEFT);
+ BannerPattern topRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_RIGHT);
+ BannerPattern bottomLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_LEFT);
+ BannerPattern bottomRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_RIGHT);
+ itemStack.set(DataComponents.BANNER_PATTERNS,
+ new BannerPatternLayers(
+ List.of(
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfLeft), DyeColor.BLACK),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topLeft), DyeColor.MAGENTA),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomLeft), DyeColor.MAGENTA),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topRight), DyeColor.BLACK),
+ new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomRight), DyeColor.BLACK)
+ )
+ )
+ );
+ itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
+ addModelData(itemStack);
+ return itemStack;
+ }
+
+}
diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java
new file mode 100644
index 00000000..61d78002
--- /dev/null
+++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java
@@ -0,0 +1,58 @@
+package com.lishid.openinv.internal.paper1_21_1.player;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.ChatVisiblity;
+import org.bukkit.OfflinePlayer;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Logger;
+
+public class PlayerManager extends com.lishid.openinv.internal.paper1_21_4.player.PlayerManager {
+
+ public PlayerManager(@NotNull Logger logger) {
+ super(logger);
+ }
+
+ @Override
+ protected @NotNull ServerPlayer createNewPlayer(
+ @NotNull MinecraftServer server,
+ @NotNull ServerLevel worldServer,
+ @NotNull final OfflinePlayer offline
+ ) {
+ // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
+ // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
+ GameProfile profile = new GameProfile(offline.getUniqueId(),
+ offline.getName() != null ? offline.getName() : offline.getUniqueId().toString()
+ );
+
+ ClientInformation dummyInfo = new ClientInformation(
+ "en_us",
+ 1, // Reduce distance just in case.
+ ChatVisiblity.HIDDEN, // Don't accept chat.
+ false,
+ ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
+ ServerPlayer.DEFAULT_MAIN_HAND,
+ true,
+ false // Don't list in player list (not that this player is in the list anyway).
+ );
+
+ ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
+
+ try {
+ injectPlayer(server, entity);
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ }
+
+ return entity;
+ }
+
+}
diff --git a/internal/paper1_21_10/build.gradle.kts b/internal/paper1_21_10/build.gradle.kts
new file mode 100644
index 00000000..d727317a
--- /dev/null
+++ b/internal/paper1_21_10/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+
+ paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java
new file mode 100644
index 00000000..b6333bf0
--- /dev/null
+++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java
@@ -0,0 +1,31 @@
+package com.lishid.openinv.internal.paper1_21_10;
+
+import com.lishid.openinv.internal.ISpecialPlayerInventory;
+import com.lishid.openinv.internal.paper1_21_10.container.OpenInventory;
+import com.lishid.openinv.internal.paper1_21_10.player.PlayerManager;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Logger;
+
+public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor {
+
+ private final @NotNull PlayerManager manager;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ super(logger, lang);
+ manager = new PlayerManager(logger);
+ }
+
+ @Override
+ public @NotNull PlayerManager getPlayerManager() {
+ return manager;
+ }
+
+ @Override
+ public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
+ return new OpenInventory(player);
+ }
+
+}
diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java
new file mode 100644
index 00000000..3210726c
--- /dev/null
+++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java
@@ -0,0 +1,50 @@
+package com.lishid.openinv.internal.paper1_21_10.container;
+
+import com.lishid.openinv.internal.common.container.BaseOpenInventory;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.FontDescription;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenInventory extends BaseOpenInventory {
+
+ public OpenInventory(@NotNull Player bukkitPlayer) {
+ super(bukkitPlayer);
+ }
+
+ @Override
+ public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu> menu) {
+ MutableComponent component = Component.empty();
+ // Prefix for use with custom bitmap image fonts.
+ if (owner.equals(viewer)) {
+ component.append(
+ Component.translatableWithFallback("openinv.container.inventory.self", "")
+ .withStyle(style -> style
+ .withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory")))
+ .withColor(ChatFormatting.WHITE)));
+ } else {
+ component.append(
+ Component.translatableWithFallback("openinv.container.inventory.other", "")
+ .withStyle(style -> style
+ .withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory")))
+ .withColor(ChatFormatting.WHITE)));
+ }
+ if (menu != null && menu.isViewOnly()) {
+ component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] "));
+ } else {
+ component.append(Component.translatableWithFallback("openinv.container.inventory.editable", ""));
+ }
+ // Normal title: "Inventory - OwnerName"
+ component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
+ .append(Component.translatable("container.inventory"))
+ .append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName()));
+ return component;
+ }
+
+}
diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java
new file mode 100644
index 00000000..0595e9b0
--- /dev/null
+++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java
@@ -0,0 +1,35 @@
+package com.lishid.openinv.internal.paper1_21_10.player;
+
+import com.lishid.openinv.internal.common.player.PlayerManager;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.craftbukkit.CraftServer;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+
+public class OpenPlayer extends com.lishid.openinv.internal.common.player.OpenPlayer {
+
+ protected OpenPlayer(
+ CraftServer server,
+ ServerPlayer entity,
+ PlayerManager manager
+ ) {
+ super(server, entity, manager);
+ }
+
+ @Override
+ protected void safeReplaceFile(
+ @NotNull Path dataFile,
+ @NotNull Path tempFile,
+ @NotNull Path backupFile
+ ) {
+ net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
+ }
+
+ @Override
+ protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
+ tag.remove(key);
+ }
+
+}
diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java
new file mode 100644
index 00000000..3382bfaf
--- /dev/null
+++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java
@@ -0,0 +1,26 @@
+package com.lishid.openinv.internal.paper1_21_10.player;
+
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Logger;
+
+public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager {
+
+ public PlayerManager(@NotNull Logger logger) {
+ super(logger);
+ }
+
+ @Override
+ protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+
+ bukkitEntity.set(player, new OpenPlayer(server.server, player, this));
+ }
+
+}
diff --git a/internal/paper1_21_3/build.gradle.kts b/internal/paper1_21_3/build.gradle.kts
new file mode 100644
index 00000000..86fa2f13
--- /dev/null
+++ b/internal/paper1_21_3/build.gradle.kts
@@ -0,0 +1,29 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+ implementation(project(":openinvadapterpaper1_21_8"))
+ implementation(project(":openinvadapterpaper1_21_5"))
+ implementation(project(":openinvadapterpaper1_21_4"))
+
+ paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java
new file mode 100644
index 00000000..e72aeb0a
--- /dev/null
+++ b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java
@@ -0,0 +1,28 @@
+package com.lishid.openinv.internal.paper1_21_3;
+
+import com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder.NumericDataPlaceholderLoader;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.configuration.ConfigurationSection;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class InternalAccessor extends com.lishid.openinv.internal.paper1_21_4.InternalAccessor {
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ super(logger, lang);
+ }
+
+ @Override
+ public void reload(@NotNull ConfigurationSection config) {
+ ConfigurationSection placeholders = config.getConfigurationSection("placeholders");
+ try {
+ // Reset placeholders to defaults and try to load configuration.
+ new NumericDataPlaceholderLoader().load(placeholders);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e);
+ }
+ }
+
+}
diff --git a/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java
new file mode 100644
index 00000000..d19552a4
--- /dev/null
+++ b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java
@@ -0,0 +1,12 @@
+package com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder;
+
+import com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder.CustomModelBase;
+import net.minecraft.world.item.component.CustomModelData;
+
+public class NumericDataPlaceholderLoader extends CustomModelBase {
+
+ public NumericDataPlaceholderLoader() {
+ super(new CustomModelData(9999));
+ }
+
+}
diff --git a/internal/paper1_21_4/build.gradle.kts b/internal/paper1_21_4/build.gradle.kts
new file mode 100644
index 00000000..761ada30
--- /dev/null
+++ b/internal/paper1_21_4/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+ implementation(project(":openinvadapterpaper1_21_8"))
+ implementation(project(":openinvadapterpaper1_21_5"))
+
+ paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java
new file mode 100644
index 00000000..98b09873
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java
@@ -0,0 +1,80 @@
+package com.lishid.openinv.internal.paper1_21_4;
+
+import com.lishid.openinv.internal.Accessor;
+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.internal.common.container.AnySilentContainer;
+import com.lishid.openinv.internal.paper1_21_4.container.OpenEnderChest;
+import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory;
+import com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder.CustomModelPlaceholderLoader;
+import com.lishid.openinv.internal.paper1_21_4.player.PlayerManager;
+import com.lishid.openinv.util.lang.LanguageManager;
+import net.minecraft.world.Container;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.craftbukkit.inventory.CraftInventory;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class InternalAccessor implements Accessor {
+
+ protected final @NotNull Logger logger;
+ private final @NotNull PlayerManager manager;
+ private final @NotNull AnySilentContainer anySilentContainer;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ this.logger = logger;
+ manager = new PlayerManager(logger);
+ anySilentContainer = new AnySilentContainer(logger, lang);
+ }
+
+ @Override
+ public @NotNull PlayerManager getPlayerManager() {
+ return manager;
+ }
+
+ @Override
+ public @NotNull IAnySilentContainer getAnySilentContainer() {
+ return anySilentContainer;
+ }
+
+ @Override
+ public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
+ return new OpenInventory(player);
+ }
+
+ @Override
+ public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) {
+ return new OpenEnderChest(player);
+ }
+
+ @Override
+ public @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz) {
+ if (!(bukkitInventory instanceof CraftInventory craftInventory)) {
+ return null;
+ }
+ Container container = craftInventory.getInventory();
+ if (clazz.isInstance(container)) {
+ return clazz.cast(container);
+ }
+ return null;
+ }
+
+ @Override
+ public void reload(@NotNull ConfigurationSection config) {
+ ConfigurationSection placeholders = config.getConfigurationSection("placeholders");
+ try {
+ // Reset placeholders to defaults and try to load configuration.
+ new CustomModelPlaceholderLoader().load(placeholders);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e);
+ }
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java
new file mode 100644
index 00000000..5ffd2665
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java
@@ -0,0 +1,28 @@
+package com.lishid.openinv.internal.paper1_21_4.container;
+
+import com.lishid.openinv.internal.paper1_21_4.container.menu.OpenEnderChestMenu;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenEnderChest extends com.lishid.openinv.internal.common.container.OpenEnderChest {
+
+ public OpenEnderChest(@NotNull Player player) {
+ super(player);
+ }
+
+ @Override
+ public @Nullable OpenEnderChestMenu createMenu(
+ net.minecraft.world.entity.player.Player player,
+ int i,
+ boolean viewOnly
+ ) {
+ if (player instanceof ServerPlayer serverPlayer) {
+ return new OpenEnderChestMenu(this, serverPlayer, i, viewOnly);
+ }
+ return null;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java
new file mode 100644
index 00000000..4eea3374
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java
@@ -0,0 +1,191 @@
+package com.lishid.openinv.internal.paper1_21_4.container;
+
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import com.lishid.openinv.internal.common.container.slot.ContentCrafting;
+import com.lishid.openinv.internal.common.container.slot.ContentCursor;
+import com.lishid.openinv.internal.common.container.slot.ContentDrop;
+import com.lishid.openinv.internal.common.container.slot.ContentList;
+import com.lishid.openinv.internal.common.container.slot.ContentViewOnly;
+import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import com.lishid.openinv.internal.paper1_21_4.container.bukkit.OpenPlayerInventory;
+import com.lishid.openinv.internal.paper1_21_4.container.menu.OpenInventoryMenu;
+import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentEquipment;
+import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentOffHand;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.player.Inventory;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenInventory extends com.lishid.openinv.internal.paper1_21_8.container.OpenInventory {
+
+ public OpenInventory(@NotNull org.bukkit.entity.Player bukkitPlayer) {
+ super(bukkitPlayer);
+ }
+
+ @Override
+ protected void setupSlots() {
+ // Top of inventory: Regular contents.
+ int nextIndex = addMainInventory();
+
+ // If inventory is expected size, we can arrange slots to be pretty.
+ Inventory ownerInv = owner.getInventory();
+ if (ownerInv.items.size() == 36
+ && ownerInv.armor.size() == 4
+ && ownerInv.offhand.size() == 1
+ && owner.inventoryMenu.getCraftSlots().getContainerSize() == 4) {
+ // Armor slots: Bottom left.
+ addArmor(36);
+ // Off-hand: Below chestplate.
+ addOffHand(46);
+ // Drop slot: Bottom right.
+ slots.set(53, new ContentDrop(owner));
+ // Cursor slot: Above drop.
+ slots.set(44, new ContentCursor(owner));
+
+ // Crafting is displayed in the bottom right corner.
+ // As we're using the pretty view, this is a 3x2.
+ addCrafting(41, true);
+ return;
+ }
+
+ // Otherwise we'll just add elements linearly.
+ nextIndex = addArmor(nextIndex);
+ nextIndex = addOffHand(nextIndex);
+ nextIndex = addCrafting(nextIndex, false);
+ slots.set(nextIndex, new ContentCursor(owner));
+ // Drop slot last.
+ slots.set(slots.size() - 1, new ContentDrop(owner));
+ }
+
+ private int addMainInventory() {
+ int listSize = owner.getInventory().items.size();
+ // Hotbar slots are 0-8. We want those to appear on the bottom of the inventory like a normal player inventory,
+ // so everything else needs to move up a row.
+ int hotbarDiff = listSize - 9;
+ for (int localIndex = 0; localIndex < listSize; ++localIndex) {
+ InventoryType.SlotType type;
+ int invIndex;
+ if (localIndex < hotbarDiff) {
+ invIndex = localIndex + 9;
+ type = InventoryType.SlotType.CONTAINER;
+ } else {
+ type = InventoryType.SlotType.QUICKBAR;
+ invIndex = localIndex - hotbarDiff;
+ }
+
+ slots.set(
+ localIndex,
+ new ContentList(owner, invIndex, type) {
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ items = holder.getInventory().items;
+ }
+ }
+ );
+ }
+ return listSize;
+ }
+
+ private int addArmor(int startIndex) {
+ int listSize = owner.getInventory().armor.size();
+
+ for (int i = 0; i < listSize; ++i) {
+ // Armor slots go bottom to top; boots are slot 0, helmet is slot 3.
+ // Since we have to display horizontally due to space restrictions,
+ // making the left side the "top" is more user-friendly.
+ int armorIndex;
+ EquipmentSlot slot;
+ switch (i) {
+ case 3 -> {
+ armorIndex = 0;
+ slot = EquipmentSlot.FEET;
+ }
+ case 2 -> {
+ armorIndex = 1;
+ slot = EquipmentSlot.LEGS;
+ }
+ case 1 -> {
+ armorIndex = 2;
+ slot = EquipmentSlot.CHEST;
+ }
+ case 0 -> {
+ armorIndex = 3;
+ slot = EquipmentSlot.HEAD;
+ }
+ default -> {
+ // In the event that new armor slots are added, they can be placed at the end.
+ armorIndex = i;
+ slot = EquipmentSlot.MAINHAND;
+ }
+ }
+
+ slots.set(startIndex + i, new ContentEquipment(owner, armorIndex, slot));
+ }
+
+ return startIndex + listSize;
+ }
+
+ private int addOffHand(int startIndex) {
+ int listSize = owner.getInventory().offhand.size();
+ for (int localIndex = 0; localIndex < listSize; ++localIndex) {
+ slots.set(startIndex + localIndex, new ContentOffHand(owner, localIndex));
+ }
+ return startIndex + listSize;
+ }
+
+ private int addCrafting(int startIndex, boolean pretty) {
+ int listSize = owner.inventoryMenu.getCraftSlots().getContents().size();
+ pretty &= listSize == 4;
+
+ for (int localIndex = 0; localIndex < listSize; ++localIndex) {
+ // Pretty display is a 2x2 rather than linear.
+ // If index is in top row, grid is not 2x2, or pretty is disabled, just use current index.
+ // Otherwise, subtract 2 and add 9 to start in the same position on the next row.
+ int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7);
+
+ slots.set(modIndex, new ContentCrafting(owner, localIndex));
+ }
+
+ if (pretty) {
+ slots.set(startIndex + 2, new ContentViewOnly(owner) {
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotViewOnly(container, slot, x, y) {
+ @Override
+ public ItemStack getOrDefault() {
+ return Placeholders.craftingOutput;
+ }
+ };
+ }
+ }
+ );
+ slots.set(startIndex + 11, getCraftingResult(owner));
+ }
+
+ return startIndex + listSize;
+ }
+
+ @Override
+ public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() {
+ if (bukkitEntity == null) {
+ bukkitEntity = new OpenPlayerInventory(this);
+ }
+ return bukkitEntity;
+ }
+
+ @Override
+ public @Nullable OpenChestMenu> createMenu(Player player, int i, boolean viewOnly) {
+ if (player instanceof ServerPlayer serverPlayer) {
+ return new OpenInventoryMenu(this, serverPlayer, i, viewOnly);
+ }
+ return null;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java
new file mode 100644
index 00000000..9cd8fbc0
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java
@@ -0,0 +1,225 @@
+package com.lishid.openinv.internal.paper1_21_4.container.bukkit;
+
+import com.google.common.base.Preconditions;
+import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory;
+import net.minecraft.core.NonNullList;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.player.Inventory;
+import org.bukkit.craftbukkit.inventory.CraftInventory;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenPlayerInventory extends CraftInventory implements PlayerInventory {
+
+ public OpenPlayerInventory(@NotNull OpenInventory inventory) {
+ super(inventory);
+ }
+
+ @Override
+ public @NotNull OpenInventory getInventory() {
+ return (OpenInventory) super.getInventory();
+ }
+
+ @Override
+ public ItemStack @NotNull [] getContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents());
+ }
+
+ @Override
+ public void setContents(ItemStack[] items) {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ int size = internal.getContainerSize();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+
+ for (int index = 0; index < size; ++index) {
+ if (index < items.length) {
+ internal.setItem(index, CraftItemStack.asNMSCopy(items[index]));
+ } else {
+ internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY);
+ }
+ }
+ }
+
+ @Override
+ public ItemStack @NotNull [] getStorageContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().items);
+ }
+
+ @Override
+ public void setStorageContents(ItemStack[] items) throws IllegalArgumentException {
+ NonNullList list = getInventory().getOwnerHandle().getInventory().items;
+ int size = list.size();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+ for (int index = 0; index < items.length; ++index) {
+ list.set(index, CraftItemStack.asNMSCopy(items[index]));
+ }
+ }
+
+ @Override
+ public @NotNull InventoryType getType() {
+ return InventoryType.PLAYER;
+ }
+
+ @Override
+ public @NotNull Player getHolder() {
+ return getInventory().getOwner();
+ }
+
+ @Override
+ public @NotNull ItemStack @NotNull [] getArmorContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().armor);
+ }
+
+ @Override
+ public void setArmorContents(ItemStack @NotNull [] items) {
+ NonNullList list = getInventory().getOwnerHandle().getInventory().armor;
+ int size = list.size();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+ for (int index = 0; index < items.length; ++index) {
+ list.set(index, CraftItemStack.asNMSCopy(items[index]));
+ }
+ }
+
+ @Override
+ public @NotNull ItemStack @NotNull [] getExtraContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand);
+ }
+
+ @Override
+ public void setExtraContents(ItemStack @NotNull [] items) {
+ NonNullList list = getInventory().getOwnerHandle().getInventory().offhand;
+ int size = list.size();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+ for (int index = 0; index < items.length; ++index) {
+ list.set(index, CraftItemStack.asNMSCopy(items[index]));
+ }
+ }
+
+ @Override
+ public @NotNull ItemStack getHelmet() {
+ return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
+ .getArmor(EquipmentSlot.HEAD.getIndex()));
+ }
+
+ @Override
+ public void setHelmet(@Nullable ItemStack helmet) {
+ getInventory().getOwnerHandle().getInventory().armor
+ .set(EquipmentSlot.HEAD.getIndex(), CraftItemStack.asNMSCopy(helmet));
+ }
+
+ @Override
+ public @NotNull ItemStack getChestplate() {
+ return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
+ .getArmor(EquipmentSlot.CHEST.getIndex()));
+ }
+
+ @Override
+ public void setChestplate(@Nullable ItemStack chestplate) {
+ getInventory().getOwnerHandle().getInventory().armor
+ .set(EquipmentSlot.CHEST.getIndex(), CraftItemStack.asNMSCopy(chestplate));
+ }
+
+ @Override
+ public @NotNull ItemStack getLeggings() {
+ return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
+ .getArmor(EquipmentSlot.LEGS.getIndex()));
+ }
+
+ @Override
+ public void setLeggings(@Nullable ItemStack leggings) {
+ getInventory().getOwnerHandle().getInventory().armor
+ .set(EquipmentSlot.LEGS.getIndex(), CraftItemStack.asNMSCopy(leggings));
+ }
+
+ @Override
+ public @NotNull ItemStack getBoots() {
+ return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
+ .getArmor(EquipmentSlot.FEET.getIndex()));
+ }
+
+ @Override
+ public void setBoots(@Nullable ItemStack boots) {
+ getInventory().getOwnerHandle().getInventory().armor
+ .set(EquipmentSlot.FEET.getIndex(), CraftItemStack.asNMSCopy(boots));
+ }
+
+ @Override
+ public @NotNull ItemStack getItemInMainHand() {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ return CraftItemStack.asCraftMirror(internal.getItem(internal.selected));
+ }
+
+ @Override
+ public void setItemInMainHand(@Nullable ItemStack item) {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ internal.setItem(internal.selected, CraftItemStack.asNMSCopy(item));
+ }
+
+ @Override
+ public @NotNull ItemStack getItemInOffHand() {
+ return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand.getFirst());
+ }
+
+ @Override
+ public void setItemInOffHand(@Nullable ItemStack item) {
+ getInventory().getOwnerHandle().getInventory().offhand.set(0, CraftItemStack.asNMSCopy(item));
+ }
+
+ @SuppressWarnings("InlineMeSuggester")
+ @Deprecated
+ @Override
+ public @NotNull ItemStack getItemInHand() {
+ return getItemInMainHand();
+ }
+
+ @SuppressWarnings("InlineMeSuggester")
+ @Deprecated
+ @Override
+ public void setItemInHand(@Nullable ItemStack stack) {
+ setItemInMainHand(stack);
+ }
+
+ @Override
+ public int getHeldItemSlot() {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ return internal.items.size() - 9 + internal.selected;
+ }
+
+ @Override
+ public void setHeldItemSlot(int slot) {
+ slot %= 9;
+ getInventory().getOwnerHandle().getInventory().selected = slot;
+ }
+
+ @Override
+ public @NotNull ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) {
+ return switch (slot) {
+ case HAND -> getItemInMainHand();
+ case OFF_HAND -> getItemInOffHand();
+ case FEET -> getBoots();
+ case LEGS -> getLeggings();
+ case CHEST -> getChestplate();
+ case HEAD -> getHelmet();
+ default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot);
+ };
+ }
+
+ @Override
+ public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) {
+ switch (slot) {
+ case HAND -> setItemInMainHand(item);
+ case OFF_HAND -> setItemInOffHand(item);
+ case FEET -> setBoots(item);
+ case LEGS -> setLeggings(item);
+ case CHEST -> setChestplate(item);
+ case HEAD -> setHelmet(item);
+ default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot);
+ }
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java
new file mode 100644
index 00000000..d6e0ce0b
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java
@@ -0,0 +1,26 @@
+package com.lishid.openinv.internal.paper1_21_4.container.bukkit;
+
+import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+public class OpenPlayerInventorySelf extends OpenPlayerInventory {
+
+ private final int offset;
+
+ public OpenPlayerInventorySelf(@NotNull OpenInventory inventory, int offset) {
+ super(inventory);
+ this.offset = offset;
+ }
+
+ @Override
+ public ItemStack getItem(int index) {
+ return super.getItem(offset + index);
+ }
+
+ @Override
+ public void setItem(int index, ItemStack item) {
+ super.setItem(offset + index, item);
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java
new file mode 100644
index 00000000..6ba23c14
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java
@@ -0,0 +1,61 @@
+package com.lishid.openinv.internal.paper1_21_4.container.menu;
+
+import com.lishid.openinv.internal.common.container.OpenEnderChest;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+public class OpenEnderChestMenu extends OpenSyncMenu {
+
+ public OpenEnderChestMenu(
+ @NotNull OpenEnderChest enderChest,
+ @NotNull ServerPlayer viewer,
+ int containerId,
+ boolean viewOnly
+ ) {
+ super(
+ OpenChestMenu.getChestMenuType(enderChest.getContainerSize()),
+ containerId,
+ enderChest,
+ viewer,
+ viewOnly
+ );
+ }
+
+ @Override
+ public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) {
+ if (viewOnly) {
+ return ItemStack.EMPTY;
+ }
+
+ // See ChestMenu
+ Slot slot = this.slots.get(index);
+
+ if (slot.isFake() || !slot.hasItem()) {
+ return ItemStack.EMPTY;
+ }
+
+ ItemStack itemStack = slot.getItem();
+ ItemStack original = itemStack.copy();
+
+ if (index < topSize) {
+ if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) {
+ return ItemStack.EMPTY;
+ }
+ } else if (!this.moveItemStackTo(itemStack, 0, topSize, false)) {
+ return ItemStack.EMPTY;
+ }
+
+ if (itemStack.isEmpty()) {
+ slot.setByPlayer(ItemStack.EMPTY);
+ } else {
+ slot.setChanged();
+ }
+
+ return original;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java
new file mode 100644
index 00000000..1bdbf469
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java
@@ -0,0 +1,266 @@
+package com.lishid.openinv.internal.paper1_21_4.container.menu;
+
+import com.google.common.base.Preconditions;
+import com.lishid.openinv.internal.common.container.bukkit.OpenDummyPlayerInventory;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import com.lishid.openinv.internal.common.container.slot.ContentDrop;
+import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
+import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory;
+import com.lishid.openinv.internal.paper1_21_4.container.bukkit.OpenPlayerInventorySelf;
+import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentEquipment;
+import com.lishid.openinv.util.Permissions;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.inventory.ChestMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenInventoryMenu extends OpenSyncMenu {
+
+ private int offset;
+
+ public OpenInventoryMenu(OpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) {
+ super(getMenuType(inventory, viewer), i, inventory, viewer, viewOnly);
+ }
+
+ private static MenuType getMenuType(OpenInventory inventory, ServerPlayer viewer) {
+ int size = inventory.getContainerSize();
+ // Disallow duplicate access to own main inventory contents.
+ if (inventory.getOwnerHandle().equals(viewer)) {
+ size -= viewer.getInventory().items.size();
+ size = ((int) Math.ceil(size / 9.0)) * 9;
+ }
+
+ return OpenChestMenu.getChestMenuType(size);
+ }
+
+ @Override
+ protected void preSlotSetup() {
+ offset = ownContainer ? viewer.getInventory().items.size() : 0;
+ }
+
+ @Override
+ protected @NotNull Slot getUpperSlot(int index, int x, int y) {
+ index += offset;
+ Slot slot = container.getMenuSlot(index, x, y);
+
+ // If the slot cannot be interacted with there's nothing to configure.
+ if (slot.getClass().equals(SlotViewOnly.class)) {
+ return slot;
+ }
+
+ // Remove drop slot if viewer is not allowed to use it.
+ if (slot instanceof ContentDrop.SlotDrop
+ && (viewOnly || !Permissions.INVENTORY_SLOT_DROP.hasPermission(viewer.getBukkitEntity()))) {
+ return new SlotViewOnly(container, index, x, y);
+ }
+
+ if (slot instanceof ContentEquipment.SlotEquipment equipment) {
+ if (viewOnly) {
+ return SlotViewOnly.wrap(slot);
+ }
+
+ Permissions perm = switch (equipment.getEquipmentSlot()) {
+ case HEAD -> Permissions.INVENTORY_SLOT_HEAD_ANY;
+ case CHEST -> Permissions.INVENTORY_SLOT_CHEST_ANY;
+ case LEGS -> Permissions.INVENTORY_SLOT_LEGS_ANY;
+ case FEET -> Permissions.INVENTORY_SLOT_FEET_ANY;
+ // Off-hand can hold anything, not just equipment.
+ default -> null;
+ };
+
+ // If the viewer doesn't have permission, only allow equipment the viewee can equip in the slot.
+ if (perm != null && !perm.hasPermission(viewer.getBukkitEntity())) {
+ equipment.onlyEquipmentFor(container.getOwnerHandle());
+ }
+
+ // Equipment slots are a core part of the inventory, so they will always be shown.
+ return slot;
+ }
+
+ // When viewing own inventory, only allow access to equipment and drop slots (equipment allowed above).
+ if (ownContainer && !(slot instanceof ContentDrop.SlotDrop)) {
+ return new SlotViewOnly(container, index, x, y);
+ }
+
+ if (viewOnly) {
+ return SlotViewOnly.wrap(slot);
+ }
+
+ return slot;
+ }
+
+ @Override
+ protected @NotNull CraftInventoryView, Inventory> createBukkitEntity() {
+ org.bukkit.inventory.Inventory bukkitInventory;
+ if (viewOnly) {
+ bukkitInventory = new OpenDummyPlayerInventory(container);
+ } else if (ownContainer) {
+ bukkitInventory = new OpenPlayerInventorySelf(container, offset);
+ } else {
+ bukkitInventory = container.getBukkitInventory();
+ }
+
+ return new CraftInventoryView<>(viewer.getBukkitEntity(), bukkitInventory, this) {
+ @Override
+ public org.bukkit.inventory.ItemStack getItem(int index) {
+ if (viewOnly || index < 0) {
+ return null;
+ }
+
+ Slot slot = slots.get(index);
+ return CraftItemStack.asCraftMirror(slot.hasItem() ? slot.getItem() : ItemStack.EMPTY);
+ }
+
+ @Override
+ public boolean isInTop(int rawSlot) {
+ return rawSlot < topSize;
+ }
+
+ @Override
+ public @Nullable Inventory getInventory(int rawSlot) {
+ if (viewOnly) {
+ return null;
+ }
+ if (rawSlot == InventoryView.OUTSIDE || rawSlot == -1) {
+ return null;
+ }
+ Preconditions.checkArgument(
+ rawSlot >= 0 && rawSlot < topSize + offset + BOTTOM_INVENTORY_SIZE,
+ "Slot %s outside of inventory",
+ rawSlot
+ );
+ if (rawSlot > topSize) {
+ return getBottomInventory();
+ }
+ Slot slot = slots.get(rawSlot);
+ if (slot.isFake()) {
+ return null;
+ }
+ return getTopInventory();
+ }
+
+ @Override
+ public int convertSlot(int rawSlot) {
+ if (viewOnly) {
+ return InventoryView.OUTSIDE;
+ }
+ if (rawSlot < 0) {
+ return rawSlot;
+ }
+ if (rawSlot < topSize) {
+ Slot slot = slots.get(rawSlot);
+ if (slot.isFake()) {
+ return InventoryView.OUTSIDE;
+ }
+ return rawSlot;
+ }
+
+ int slot = rawSlot - topSize;
+
+ if (slot >= 27) {
+ slot -= 27;
+ } else {
+ slot += 9;
+ }
+
+ return slot;
+ }
+
+ @Override
+ public @NotNull InventoryType.SlotType getSlotType(int slot) {
+ if (viewOnly || slot < 0) {
+ return InventoryType.SlotType.OUTSIDE;
+ }
+ if (slot >= topSize) {
+ slot -= topSize;
+ if (slot >= 27) {
+ return InventoryType.SlotType.QUICKBAR;
+ }
+ return InventoryType.SlotType.CONTAINER;
+ }
+ return OpenInventoryMenu.this.container.getSlotType(offset + slot);
+ }
+
+ @Override
+ public int countSlots() {
+ return topSize + BOTTOM_INVENTORY_SIZE;
+ }
+ };
+ }
+
+ @Override
+ public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) {
+ if (viewOnly) {
+ return ItemStack.EMPTY;
+ }
+
+ // See ChestMenu and InventoryMenu
+ Slot slot = this.slots.get(index);
+
+ if (!slot.hasItem() || slot.isFake()) {
+ return ItemStack.EMPTY;
+ }
+
+ ItemStack itemStack = slot.getItem();
+ ItemStack originalStack = itemStack.copy();
+
+ if (index < topSize) {
+ // If we're moving top to bottom, do a normal transfer.
+ if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) {
+ return ItemStack.EMPTY;
+ }
+ } else {
+ EquipmentSlot equipmentSlot = player.getEquipmentSlotForItem(itemStack);
+ boolean movedGear = switch (equipmentSlot) {
+ // If this is gear, try to move it to the correct slot first.
+ case OFFHAND, FEET, LEGS, CHEST, HEAD -> {
+ // Locate the correct slot in the contents following the main inventory.
+ for (int extra = container.getOwnerHandle().getInventory().items.size() - offset; extra < topSize; ++extra) {
+ Slot extraSlot = getSlot(extra);
+ if (extraSlot instanceof ContentEquipment.SlotEquipment equipSlot
+ && equipSlot.getEquipmentSlot() == equipmentSlot) {
+ // If we've found a matching slot, try to move to it.
+ // If this succeeds, even partially, we will not attempt to move to other slots.
+ // Otherwise, armor is already occupied, so we'll fall through to main inventory.
+ yield this.moveItemStackTo(itemStack, extra, extra + 1, false);
+ }
+ }
+ yield false;
+ }
+ // Non-gear gets no special treatment.
+ default -> false;
+ };
+
+ // If main inventory is not available, there's nowhere else to move.
+ if (offset != 0) {
+ if (!movedGear) {
+ return ItemStack.EMPTY;
+ }
+ } else {
+ // If we didn't move to a gear slot, try to move to a main inventory slot.
+ if (!movedGear && !this.moveItemStackTo(itemStack, 0, container.getOwnerHandle().getInventory().items.size(), true)) {
+ return ItemStack.EMPTY;
+ }
+ }
+ }
+
+ if (itemStack.isEmpty()) {
+ slot.setByPlayer(ItemStack.EMPTY);
+ } else {
+ slot.setChanged();
+ }
+
+ return originalStack;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java
new file mode 100644
index 00000000..5a2d6162
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java
@@ -0,0 +1,229 @@
+package com.lishid.openinv.internal.paper1_21_4.container.menu;
+
+import com.google.common.base.Suppliers;
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.internal.InternalOwned;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.ChestMenu;
+import net.minecraft.world.inventory.ContainerData;
+import net.minecraft.world.inventory.ContainerListener;
+import net.minecraft.world.inventory.ContainerSynchronizer;
+import net.minecraft.world.inventory.DataSlot;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}.
+ */
+@SuppressWarnings("HidingField")
+public abstract class OpenSyncMenu>
+ extends OpenChestMenu {
+
+ // Syncher fields
+ private @Nullable ContainerSynchronizer synchronizer;
+ private final List dataSlots = new ArrayList<>();
+ private final IntList remoteDataSlots = new IntArrayList();
+ private final List containerListeners = new ArrayList<>();
+ private ItemStack remoteCarried = ItemStack.EMPTY;
+ private boolean suppressRemoteUpdates;
+
+ protected OpenSyncMenu(
+ @NotNull MenuType type,
+ int containerCounter,
+ @NotNull T container,
+ @NotNull ServerPlayer viewer,
+ boolean viewOnly
+ ) {
+ super(type, containerCounter, container, viewer, viewOnly);
+ }
+
+ // Overrides from here on are purely to modify the sync process to send placeholder items.
+ @Override
+ protected @NotNull Slot addSlot(@NotNull Slot slot) {
+ slot.index = this.slots.size();
+ this.slots.add(slot);
+ this.lastSlots.add(ItemStack.EMPTY);
+ this.remoteSlots.add(ItemStack.EMPTY);
+ return slot;
+ }
+
+ @Override
+ protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) {
+ this.dataSlots.add(dataSlot);
+ this.remoteDataSlots.add(0);
+ return dataSlot;
+ }
+
+ @Override
+ protected void addDataSlots(ContainerData containerData) {
+ for (int i = 0; i < containerData.getCount(); i++) {
+ this.addDataSlot(DataSlot.forContainer(containerData, i));
+ }
+ }
+
+ @Override
+ public void addSlotListener(@NotNull ContainerListener containerListener) {
+ if (!this.containerListeners.contains(containerListener)) {
+ this.containerListeners.add(containerListener);
+ this.broadcastChanges();
+ }
+ }
+
+ @Override
+ public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) {
+ this.synchronizer = containerSynchronizer;
+ this.sendAllDataToRemote();
+ }
+
+ @Override
+ public void sendAllDataToRemote() {
+ for (int index = 0; index < slots.size(); ++index) {
+ Slot slot = slots.get(index);
+ this.remoteSlots.set(index, (slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem()).copy());
+ }
+
+ remoteCarried = getCarried().copy();
+
+ for (int index = 0; index < this.dataSlots.size(); ++index) {
+ this.remoteDataSlots.set(index, this.dataSlots.get(index).get());
+ }
+
+ if (this.synchronizer != null) {
+ this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
+ }
+ }
+
+ @Override
+ public void broadcastCarriedItem() {
+ this.remoteCarried = this.getCarried().copy();
+ if (this.synchronizer != null) {
+ this.synchronizer.sendCarriedChange(this, this.remoteCarried);
+ }
+ }
+
+ @Override
+ public void removeSlotListener(@NotNull ContainerListener containerListener) {
+ this.containerListeners.remove(containerListener);
+ }
+
+ @Override
+ public void broadcastChanges() {
+ for (int index = 0; index < this.slots.size(); ++index) {
+ Slot slot = this.slots.get(index);
+ ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
+ Supplier supplier = Suppliers.memoize(itemstack::copy);
+ this.triggerSlotListeners(index, itemstack, supplier);
+ this.synchronizeSlotToRemote(index, itemstack, supplier);
+ }
+
+ this.synchronizeCarriedToRemote();
+
+ for (int index = 0; index < this.dataSlots.size(); ++index) {
+ DataSlot dataSlot = this.dataSlots.get(index);
+ int j = dataSlot.get();
+ if (dataSlot.checkAndClearUpdateFlag()) {
+ this.updateDataSlotListeners(index, j);
+ }
+
+ this.synchronizeDataSlotToRemote(index, j);
+ }
+ }
+
+ @Override
+ public void broadcastFullState() {
+ for (int index = 0; index < this.slots.size(); ++index) {
+ ItemStack itemstack = this.slots.get(index).getItem();
+ this.triggerSlotListeners(index, itemstack, itemstack::copy);
+ }
+
+ for (int index = 0; index < this.dataSlots.size(); ++index) {
+ DataSlot containerproperty = this.dataSlots.get(index);
+ if (containerproperty.checkAndClearUpdateFlag()) {
+ this.updateDataSlotListeners(index, containerproperty.get());
+ }
+ }
+
+ this.sendAllDataToRemote();
+ }
+
+ private void updateDataSlotListeners(int i, int j) {
+ for (ContainerListener containerListener : this.containerListeners) {
+ containerListener.dataChanged(this, i, j);
+ }
+ }
+
+ private void triggerSlotListeners(int index, ItemStack itemStack, Supplier supplier) {
+ ItemStack itemStack1 = this.lastSlots.get(index);
+ if (!ItemStack.matches(itemStack1, itemStack)) {
+ ItemStack itemStack2 = supplier.get();
+ this.lastSlots.set(index, itemStack2);
+
+ for (ContainerListener containerListener : this.containerListeners) {
+ containerListener.slotChanged(this, index, itemStack2);
+ }
+ }
+ }
+
+ private void synchronizeSlotToRemote(int i, ItemStack itemStack, Supplier supplier) {
+ if (!this.suppressRemoteUpdates) {
+ ItemStack itemStack1 = this.remoteSlots.get(i);
+ if (!ItemStack.matches(itemStack1, itemStack)) {
+ ItemStack itemstack2 = supplier.get();
+ this.remoteSlots.set(i, itemstack2);
+ if (this.synchronizer != null) {
+ this.synchronizer.sendSlotChange(this, i, itemstack2);
+ }
+ }
+ }
+ }
+
+ private void synchronizeDataSlotToRemote(int index, int value) {
+ if (!this.suppressRemoteUpdates) {
+ int existing = this.remoteDataSlots.getInt(index);
+ if (existing != value) {
+ this.remoteDataSlots.set(index, value);
+ if (this.synchronizer != null) {
+ this.synchronizer.sendDataChange(this, index, value);
+ }
+ }
+ }
+ }
+
+ private void synchronizeCarriedToRemote() {
+ if (!this.suppressRemoteUpdates && !ItemStack.matches(this.getCarried(), this.remoteCarried)) {
+ this.remoteCarried = this.getCarried().copy();
+ if (this.synchronizer != null) {
+ this.synchronizer.sendCarriedChange(this, this.remoteCarried);
+ }
+ }
+ }
+
+ @Override
+ public void setRemoteCarried(ItemStack itemstack) {
+ this.remoteCarried = itemstack.copy();
+ }
+
+ @Override
+ public void suppressRemoteUpdates() {
+ this.suppressRemoteUpdates = true;
+ }
+
+ @Override
+ public void resumeRemoteUpdates() {
+ this.suppressRemoteUpdates = false;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java
new file mode 100644
index 00000000..b5513499
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java
@@ -0,0 +1,80 @@
+package com.lishid.openinv.internal.paper1_21_4.container.slot;
+
+import com.lishid.openinv.internal.common.container.slot.ContentList;
+import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
+import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A slot for equipment that displays placeholders if empty.
+ */
+public class ContentEquipment extends ContentList {
+
+ private final ItemStack placeholder;
+ private final EquipmentSlot equipmentSlot;
+
+ public ContentEquipment(ServerPlayer holder, int index, EquipmentSlot equipmentSlot) {
+ super(holder, index, InventoryType.SlotType.ARMOR);
+ placeholder = switch (equipmentSlot) {
+ case HEAD -> Placeholders.emptyHelmet;
+ case CHEST -> Placeholders.emptyChestplate;
+ case LEGS -> Placeholders.emptyLeggings;
+ case FEET -> Placeholders.emptyBoots;
+ default -> Placeholders.emptyOffHand;
+ };
+ this.equipmentSlot = equipmentSlot;
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.items = holder.getInventory().armor;
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y);
+ }
+
+ public class SlotEquipment extends SlotPlaceholder {
+
+ private ServerPlayer viewer;
+
+ SlotEquipment(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ ItemStack itemStack = getItem();
+ if (!itemStack.isEmpty()) {
+ return itemStack;
+ }
+ return placeholder;
+ }
+
+ public EquipmentSlot getEquipmentSlot() {
+ return equipmentSlot;
+ }
+
+ public void onlyEquipmentFor(ServerPlayer viewer) {
+ this.viewer = viewer;
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ if (viewer == null) {
+ return true;
+ }
+
+ return equipmentSlot == EquipmentSlot.OFFHAND || viewer.getEquipmentSlotForItem(itemStack) == equipmentSlot;
+ }
+
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java
new file mode 100644
index 00000000..31da3618
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java
@@ -0,0 +1,53 @@
+package com.lishid.openinv.internal.paper1_21_4.container.slot;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.InventoryMenu;
+import net.minecraft.world.inventory.Slot;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A slot for equipment that updates held items if necessary.
+ */
+public class ContentOffHand extends ContentEquipment {
+
+ private ServerPlayer holder;
+
+ public ContentOffHand(ServerPlayer holder, int localIndex) {
+ super(holder, localIndex, EquipmentSlot.OFFHAND);
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.items = holder.getInventory().offhand;
+ this.holder = holder;
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.QUICKBAR;
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y) {
+ @Override
+ public void setChanged() {
+ if (BaseOpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) {
+ holder.connection.send(
+ new ClientboundContainerSetSlotPacket(
+ holder.inventoryMenu.containerId,
+ holder.inventoryMenu.incrementStateId(),
+ InventoryMenu.SHIELD_SLOT,
+ holder.getOffhandItem()
+ ));
+ }
+ }
+ };
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java
new file mode 100644
index 00000000..33dd3a21
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java
@@ -0,0 +1,41 @@
+package com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.PlaceholderLoaderBase;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.TagParser;
+import net.minecraft.util.Unit;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.CustomModelData;
+import net.minecraft.world.item.component.DyedItemColor;
+import org.jetbrains.annotations.NotNull;
+
+public class CustomModelBase extends PlaceholderLoaderBase {
+
+ private final @NotNull CustomModelData defaultCustomModelData;
+
+ public CustomModelBase(@NotNull CustomModelData defaultCustomModelData) {
+ this.defaultCustomModelData = defaultCustomModelData;
+ }
+
+ @Override
+ protected @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception {
+ return TagParser.parseTag(itemText);
+ }
+
+ @Override
+ protected void addModelData(@NotNull ItemStack itemStack) {
+ itemStack.set(DataComponents.CUSTOM_MODEL_DATA, defaultCustomModelData);
+ }
+
+ @Override
+ protected void hideTooltip(@NotNull ItemStack itemStack) {
+ itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
+ }
+
+ @Override
+ protected DyedItemColor getDye(int rgb) {
+ return new DyedItemColor(rgb, false);
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java
new file mode 100644
index 00000000..d6c2c0b5
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java
@@ -0,0 +1,13 @@
+package com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder;
+
+import net.minecraft.world.item.component.CustomModelData;
+
+import java.util.List;
+
+public class CustomModelPlaceholderLoader extends CustomModelBase {
+
+ public CustomModelPlaceholderLoader() {
+ super(new CustomModelData(List.of(), List.of(), List.of("openinv:custom"), List.of()));
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java
new file mode 100644
index 00000000..3f1fe082
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java
@@ -0,0 +1,36 @@
+package com.lishid.openinv.internal.paper1_21_4.player;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.craftbukkit.CraftServer;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenPlayer extends com.lishid.openinv.internal.paper1_21_5.player.OpenPlayer {
+
+ protected OpenPlayer(
+ CraftServer server, ServerPlayer entity,
+ PlayerManager manager
+ ) {
+ super(server, entity, manager);
+ }
+
+ @Contract("null -> new")
+ @Override
+ protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
+ if (oldData == null) {
+ return new CompoundTag();
+ }
+
+ // Copy old data. This is a deep clone, so operating on it should be safe.
+ oldData = oldData.copy();
+
+ // Remove vanilla/server data that is not written every time.
+ oldData.getAllKeys()
+ .removeIf(key -> RESET_TAGS.contains(key) || key.startsWith("Bukkit"));
+
+ return oldData;
+ }
+
+}
diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java
new file mode 100644
index 00000000..4172afd7
--- /dev/null
+++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java
@@ -0,0 +1,73 @@
+package com.lishid.openinv.internal.paper1_21_4.player;
+
+import com.mojang.serialization.Dynamic;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.dimension.DimensionType;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+import java.util.logging.Logger;
+
+public class PlayerManager extends com.lishid.openinv.internal.paper1_21_5.player.PlayerManager {
+
+ public PlayerManager(@NotNull Logger logger) {
+ super(logger);
+ }
+
+ @Override
+ protected void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) {
+ // See PlayerList#placeNewPlayer
+ World bukkitWorld;
+ if (loadedData.contains("WorldUUIDMost") && loadedData.contains("WorldUUIDLeast")) {
+ // Modern Bukkit world.
+ bukkitWorld = Bukkit.getServer().getWorld(new UUID(loadedData.getLong("WorldUUIDMost"), loadedData.getLong("WorldUUIDLeast")));
+ } else if (loadedData.contains("world", net.minecraft.nbt.Tag.TAG_STRING)) {
+ // Legacy Bukkit world.
+ bukkitWorld = Bukkit.getServer().getWorld(loadedData.getString("world"));
+ } else {
+ // Vanilla player data.
+ DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension")))
+ .resultOrPartial(logger::warning)
+ .map(player.server::getLevel)
+ // If ServerLevel exists, set, otherwise move to spawn.
+ .ifPresentOrElse(player::setServerLevel, () -> spawnInDefaultWorld(player.server, player));
+ return;
+ }
+ if (bukkitWorld == null) {
+ spawnInDefaultWorld(player.server, player);
+ return;
+ }
+ player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
+ }
+
+ @Override
+ protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ ServerLevel level = server.getLevel(Level.OVERWORLD);
+ if (level != null) {
+ // Adjust player to default spawn (in keeping with Paper handling) when world not found.
+ player.moveTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F);
+ player.spawnIn(level);
+ } else {
+ logger.warning("Tried to load player with invalid world when no fallback was available!");
+ }
+ }
+
+ @Override
+ protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+ bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
+ }
+
+}
diff --git a/internal/paper1_21_5/build.gradle.kts b/internal/paper1_21_5/build.gradle.kts
new file mode 100644
index 00000000..2b1471c1
--- /dev/null
+++ b/internal/paper1_21_5/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+ implementation(project(":openinvadapterpaper1_21_8"))
+
+ paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java
new file mode 100644
index 00000000..e0552bd2
--- /dev/null
+++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java
@@ -0,0 +1,37 @@
+package com.lishid.openinv.internal.paper1_21_5;
+
+import com.lishid.openinv.internal.paper1_21_5.container.slot.placeholder.PlaceholderLoaderLegacyParse;
+import com.lishid.openinv.internal.paper1_21_5.player.PlayerManager;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.configuration.ConfigurationSection;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class InternalAccessor extends com.lishid.openinv.internal.paper1_21_8.InternalAccessor {
+
+ private final @NotNull PlayerManager manager;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ super(logger, lang);
+ manager = new PlayerManager(logger);
+ }
+
+ @Override
+ public @NotNull PlayerManager getPlayerManager() {
+ return manager;
+ }
+
+ @Override
+ public void reload(@NotNull ConfigurationSection config) {
+ ConfigurationSection placeholders = config.getConfigurationSection("placeholders");
+ try {
+ // Reset placeholders to defaults and try to load configuration.
+ new PlaceholderLoaderLegacyParse().load(placeholders);
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e);
+ }
+ }
+
+}
diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java
new file mode 100644
index 00000000..3b11c697
--- /dev/null
+++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java
@@ -0,0 +1,36 @@
+package com.lishid.openinv.internal.paper1_21_5.container.slot.placeholder;
+
+import com.lishid.openinv.internal.common.container.slot.placeholder.PlaceholderLoader;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.craftbukkit.CraftRegistry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+
+public class PlaceholderLoaderLegacyParse extends PlaceholderLoader {
+
+ @Override
+ protected @NotNull ItemStack parse(
+ @Nullable ConfigurationSection section,
+ @NotNull String path,
+ @NotNull ItemStack defaultStack
+ ) throws Exception {
+ if (section == null) {
+ return defaultStack;
+ }
+
+ String itemText = section.getString(path);
+
+ if (itemText == null) {
+ return defaultStack;
+ }
+
+ CompoundTag compoundTag = parseTag(itemText);
+ Optional parsed = ItemStack.parse(CraftRegistry.getMinecraftRegistry(), compoundTag);
+ return parsed.filter(itemStack -> !itemStack.isEmpty()).orElse(defaultStack);
+ }
+
+}
diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java
new file mode 100644
index 00000000..46b4aa87
--- /dev/null
+++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java
@@ -0,0 +1,46 @@
+package com.lishid.openinv.internal.paper1_21_5.player;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.storage.PlayerDataStorage;
+import org.bukkit.craftbukkit.CraftServer;
+import org.jetbrains.annotations.NotNull;
+
+import java.nio.file.Path;
+
+public class OpenPlayer extends BaseOpenPlayer {
+
+ protected OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
+ super(server, entity, manager);
+ }
+
+ @Override
+ protected void trySave(ServerPlayer player) {
+ // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
+ try {
+ PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
+
+ CompoundTag oldData = isOnline() ? null : worldNBTStorage.load(player.getName().getString(), player.getStringUUID()).orElse(null);
+ CompoundTag playerData = getWritableTag(oldData);
+
+ playerData = player.saveWithoutId(playerData);
+
+ saveSafe(player, oldData, playerData, worldNBTStorage);
+ } catch (Exception e) {
+ LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
+ }
+ }
+
+ @Override
+ protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) {
+ net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
+ }
+
+ @Override
+ protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
+ tag.remove(key);
+ }
+
+}
diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java
new file mode 100644
index 00000000..9637b062
--- /dev/null
+++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java
@@ -0,0 +1,106 @@
+package com.lishid.openinv.internal.paper1_21_5.player;
+
+import com.mojang.serialization.Dynamic;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.dimension.DimensionType;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.UUID;
+import java.util.logging.Logger;
+
+public class PlayerManager extends com.lishid.openinv.internal.paper1_21_8.player.PlayerManager {
+
+ public PlayerManager(@NotNull Logger logger) {
+ super(logger);
+ }
+
+ @Override
+ protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ // See CraftPlayer#loadData
+ CompoundTag loadedData = server.getPlayerList().playerIo.load(player).orElse(null);
+
+ if (loadedData == null) {
+ // Exceptions with loading are logged.
+ return false;
+ }
+
+ // Read basic data into the player.
+ player.load(loadedData);
+ // Game type settings are also loaded separately.
+ player.loadGameTypes(loadedData);
+
+ // World is not loaded by ServerPlayer#load(CompoundTag) on Paper.
+ parseWorld(player, loadedData);
+
+ return true;
+ }
+
+ protected void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) {
+ // See PlayerList#placeNewPlayer
+ World bukkitWorld;
+ Optional msbs = loadedData.getLong("WorldUUIDMost");
+ Optional lsbs = loadedData.getLong("WorldUUIDLeast");
+ if (msbs.isPresent() && lsbs.isPresent()) {
+ // Modern Bukkit world.
+ bukkitWorld = Bukkit.getServer().getWorld(new UUID(msbs.get(), lsbs.get()));
+ } else {
+ Optional worldName = loadedData.getString("world");
+ if (worldName.isPresent()) {
+ // Legacy Bukkit world.
+ bukkitWorld = Bukkit.getServer().getWorld(worldName.get());
+ } else {
+ // Vanilla player data.
+ DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension")))
+ .resultOrPartial(logger::warning)
+ .map(player.server::getLevel)
+ // If ServerLevel exists, set, otherwise move to spawn.
+ .ifPresentOrElse(player::setServerLevel, () -> spawnInDefaultWorld(player.server, player));
+ return;
+ }
+ }
+ if (bukkitWorld == null) {
+ spawnInDefaultWorld(player.server, player);
+ return;
+ }
+ player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
+ }
+
+ @Override
+ public @NotNull Player inject(@NotNull Player player) {
+ try {
+ ServerPlayer nmsPlayer = getHandle(player);
+ if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) {
+ return openPlayer;
+ }
+ injectPlayer(nmsPlayer.server, nmsPlayer);
+ return nmsPlayer.getBukkitEntity();
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ return player;
+ }
+ }
+
+ @Override
+ protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+
+ bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
+ }
+
+}
diff --git a/internal/paper1_21_8/build.gradle.kts b/internal/paper1_21_8/build.gradle.kts
new file mode 100644
index 00000000..306db103
--- /dev/null
+++ b/internal/paper1_21_8/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ `openinv-base`
+ alias(libs.plugins.paperweight)
+}
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val paper = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "paper-api"
+ }
+ }
+ if (paper != null) {
+ select(paper)
+ }
+ because("module is written for Paper servers")
+ }
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+
+ paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT")
+}
diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java
new file mode 100644
index 00000000..0c68893d
--- /dev/null
+++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java
@@ -0,0 +1,31 @@
+package com.lishid.openinv.internal.paper1_21_8;
+
+import com.lishid.openinv.internal.ISpecialPlayerInventory;
+import com.lishid.openinv.internal.paper1_21_8.container.OpenInventory;
+import com.lishid.openinv.internal.paper1_21_8.player.PlayerManager;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Logger;
+
+public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor {
+
+ private final @NotNull PlayerManager manager;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ super(logger, lang);
+ manager = new PlayerManager(logger);
+ }
+
+ @Override
+ public @NotNull PlayerManager getPlayerManager() {
+ return manager;
+ }
+
+ @Override
+ public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
+ return new OpenInventory(player);
+ }
+
+}
diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java
new file mode 100644
index 00000000..076d69a8
--- /dev/null
+++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java
@@ -0,0 +1,49 @@
+package com.lishid.openinv.internal.paper1_21_8.container;
+
+import com.lishid.openinv.internal.common.container.BaseOpenInventory;
+import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.MutableComponent;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenInventory extends BaseOpenInventory {
+
+ public OpenInventory(@NotNull Player bukkitPlayer) {
+ super(bukkitPlayer);
+ }
+
+ @Override
+ public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu> menu) {
+ MutableComponent component = Component.empty();
+ // Prefix for use with custom bitmap image fonts.
+ if (owner.equals(viewer)) {
+ component.append(
+ Component.translatableWithFallback("openinv.container.inventory.self", "")
+ .withStyle(style -> style
+ .withFont(ResourceLocation.parse("openinv:font/inventory"))
+ .withColor(ChatFormatting.WHITE)));
+ } else {
+ component.append(
+ Component.translatableWithFallback("openinv.container.inventory.other", "")
+ .withStyle(style -> style
+ .withFont(ResourceLocation.parse("openinv:font/inventory"))
+ .withColor(ChatFormatting.WHITE)));
+ }
+ if (menu != null && menu.isViewOnly()) {
+ component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] "));
+ } else {
+ component.append(Component.translatableWithFallback("openinv.container.inventory.editable", ""));
+ }
+ // Normal title: "Inventory - OwnerName"
+ component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
+ .append(Component.translatable("container.inventory"))
+ .append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName()));
+ return component;
+ }
+
+}
diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java
new file mode 100644
index 00000000..0cad37a7
--- /dev/null
+++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java
@@ -0,0 +1,58 @@
+package com.lishid.openinv.internal.paper1_21_8.player;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.world.level.storage.PlayerDataStorage;
+import net.minecraft.world.level.storage.TagValueOutput;
+import net.minecraft.world.level.storage.ValueOutput;
+import org.bukkit.craftbukkit.CraftServer;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+
+import java.nio.file.Path;
+
+public class OpenPlayer extends BaseOpenPlayer {
+
+ protected OpenPlayer(
+ CraftServer server,
+ ServerPlayer entity,
+ PlayerManager manager
+ ) {
+ super(server, entity, manager);
+ }
+
+ @Override
+ protected void trySave(ServerPlayer player) {
+ Logger logger = LogUtils.getLogger();
+ // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
+ PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo;
+
+ CompoundTag oldData = isOnline()
+ ? null
+ : worldNbtStorage.load(player.getName().getString(), player.getStringUUID(), scopedCollector).orElse(null);
+ CompoundTag playerData = getWritableTag(oldData);
+
+ ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData);
+ player.saveWithoutId(valueOutput);
+
+ saveSafe(player, oldData, playerData, worldNbtStorage);
+ } catch (Exception e) {
+ LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
+ }
+ }
+
+ @Override
+ protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) {
+ net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
+ }
+
+ @Override
+ protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
+ tag.remove(key);
+ }
+
+}
diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java
new file mode 100644
index 00000000..dc3ad219
--- /dev/null
+++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java
@@ -0,0 +1,100 @@
+package com.lishid.openinv.internal.paper1_21_8.player;
+
+import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
+import com.lishid.openinv.util.JulLoggerAdapter;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.storage.ValueInput;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.CraftServer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Logger;
+
+public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager {
+
+ public PlayerManager(@NotNull Logger logger) {
+ super(logger);
+ }
+
+ @Override
+ protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ // See CraftPlayer#loadData
+
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
+ ValueInput loadedData = server.getPlayerList().playerIo.load(player, scopedCollector).orElse(null);
+
+ if (loadedData == null) {
+ // Exceptions with loading are logged.
+ return false;
+ }
+
+ // Read basic data into the player.
+ player.load(loadedData);
+ // Game type settings are loaded separately.
+ player.loadGameTypes(loadedData);
+
+ // World is not loaded by ServerPlayer#load(CompoundTag) on Paper.
+ parseWorld(server, player, loadedData);
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
+ ServerLevel level = server.getLevel(Level.OVERWORLD);
+ if (level != null) {
+ // Adjust player to default spawn (in keeping with Paper handling) when world not found.
+ player.snapTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F);
+ player.spawnIn(level);
+ } else {
+ logger.warning("Tried to load player with invalid world when no fallback was available!");
+ }
+ }
+
+ @Override
+ protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+
+ bukkitEntity.set(player, new OpenPlayer(server.server, player, this));
+ }
+
+ @Override
+ public @NotNull Player inject(@NotNull Player player) {
+ try {
+ ServerPlayer nmsPlayer = getHandle(player);
+ if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) {
+ return openPlayer;
+ }
+ MinecraftServer server = nmsPlayer.getServer();
+ if (server == null) {
+ if (!(Bukkit.getServer() instanceof CraftServer craftServer)) {
+ logger.warning(() ->
+ "Unable to inject ServerPlayer, certain player data may be lost when saving! Server is not a CraftServer: "
+ + Bukkit.getServer().getClass().getName());
+ return player;
+ }
+ server = craftServer.getServer();
+ }
+ injectPlayer(server, nmsPlayer);
+ return nmsPlayer.getBukkitEntity();
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ return player;
+ }
+ }
+
+}
diff --git a/internal/pom.xml b/internal/pom.xml
deleted file mode 100644
index fd28dce6..00000000
--- a/internal/pom.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
- 4.0.0
-
-
- com.lishid
- openinvparent
- 4.1.6-SNAPSHOT
-
-
- openinvinternal
- OpenInvInternal
-
- pom
-
-
-
-
- all
-
- v1_16_R3
-
-
-
-
-
-
diff --git a/internal/spigot/build.gradle.kts b/internal/spigot/build.gradle.kts
new file mode 100644
index 00000000..5a74fb87
--- /dev/null
+++ b/internal/spigot/build.gradle.kts
@@ -0,0 +1,44 @@
+import com.github.jikoo.openinv.SpigotDependencyExtension
+import com.github.jikoo.openinv.SpigotReobf
+import com.github.jikoo.openinv.SpigotSetup
+
+plugins {
+ `openinv-base`
+ alias(libs.plugins.shadow)
+}
+
+apply()
+apply()
+
+val spigotVer = "1.21.11-R0.1-SNAPSHOT"
+// Used by common adapter to relocate Craftbukkit classes to a versioned package.
+rootProject.extra["craftbukkitPackage"] = "v1_21_R7"
+
+configurations.all {
+ resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
+ val spigot = candidates.firstOrNull {
+ it.id.let { id ->
+ id is ModuleComponentIdentifier && id.module == "spigot-api"
+ }
+ }
+ if (spigot != null) {
+ select(spigot)
+ }
+ because("module is written for Spigot servers")
+ }
+}
+
+dependencies {
+ compileOnly(libs.spigotapi)
+ extensions.getByType(SpigotDependencyExtension::class.java).version = spigotVer
+
+ compileOnly(project(":openinvapi"))
+ compileOnly(project(":openinvcommon"))
+
+ // Reduce duplicate code by lightly remapping common adapter.
+ implementation(project(":openinvadaptercommon", configuration = "spigotRelocated"))
+}
+
+tasks.shadowJar {
+ relocate("com.lishid.openinv.internal.common", "com.lishid.openinv.internal.reobf")
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java
new file mode 100644
index 00000000..5b3c1559
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java
@@ -0,0 +1,200 @@
+package com.lishid.openinv.internal.reobf.container;
+
+import com.lishid.openinv.internal.AnySilentContainerBase;
+import com.lishid.openinv.internal.reobf.container.menu.OpenChestMenu;
+import com.lishid.openinv.internal.reobf.player.PlayerManager;
+import com.lishid.openinv.util.ReflectionHelper;
+import com.lishid.openinv.util.lang.LanguageManager;
+import net.minecraft.core.BlockPos;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.level.ServerPlayerGameMode;
+import net.minecraft.world.MenuProvider;
+import net.minecraft.world.SimpleMenuProvider;
+import net.minecraft.world.inventory.ChestMenu;
+import net.minecraft.world.inventory.MenuType;
+import net.minecraft.world.inventory.PlayerEnderChestContainer;
+import net.minecraft.world.level.GameType;
+import net.minecraft.world.level.block.BarrelBlock;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.ChestBlock;
+import net.minecraft.world.level.block.ShulkerBoxBlock;
+import net.minecraft.world.level.block.TrappedChestBlock;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
+import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import org.bukkit.GameMode;
+import org.bukkit.Material;
+import org.bukkit.Statistic;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Field;
+import java.util.logging.Logger;
+
+public class AnySilentContainer extends AnySilentContainerBase {
+
+ private final @NotNull Logger logger;
+ private final @NotNull LanguageManager lang;
+ private @Nullable Field serverPlayerGameModeGameType;
+
+ public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ this.logger = logger;
+ this.lang = lang;
+ try {
+ try {
+ this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("gameModeForPlayer");
+ this.serverPlayerGameModeGameType.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ logger.warning("The field ServerPlayerGameMode#gameModeForPlayer is no longer present!");
+ logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues");
+ logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail.");
+ // N.B. gameModeForPlayer is (for now) declared before previousGameModeForPlayer so silent shouldn't break.
+ this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class);
+ }
+ } catch (SecurityException e) {
+ logger.warning("Unable to directly write player game mode! SilentContainer will fail.");
+ logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e);
+ }
+ }
+
+ @Override
+ public boolean activateContainer(
+ @NotNull final Player bukkitPlayer,
+ final boolean silentchest,
+ @NotNull final org.bukkit.block.Block bukkitBlock
+ ) {
+
+ // Silent ender chest is API-only
+ if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) {
+ bukkitPlayer.openInventory(bukkitPlayer.getEnderChest());
+ bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
+ return true;
+ }
+
+ ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
+
+ final net.minecraft.world.level.Level level = player.level();
+ final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ());
+ final BlockEntity blockEntity = level.getBlockEntity(blockPos);
+
+ if (blockEntity == null) {
+ return false;
+ }
+
+ if (blockEntity instanceof EnderChestBlockEntity enderChestTile) {
+ // Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock
+ PlayerEnderChestContainer enderChest = player.getEnderChestInventory();
+ enderChest.setActiveChest(enderChestTile);
+ player.openMenu(
+ new SimpleMenuProvider(
+ (containerCounter, playerInventory, ignored) -> {
+ MenuType> containers = OpenChestMenu.getChestMenuType(enderChest.getContainerSize());
+ int rows = enderChest.getContainerSize() / 9;
+ return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows);
+ },
+ Component.translatable("container.enderchest")
+ )
+ );
+ bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
+ return true;
+ }
+
+ if (!(blockEntity instanceof MenuProvider menuProvider)) {
+ return false;
+ }
+
+ BlockState blockState = level.getBlockState(blockPos);
+ Block block = blockState.getBlock();
+
+ if (block instanceof ChestBlock chestBlock) {
+
+ // boolean flag: do not check if chest is blocked
+ menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true);
+
+ if (menuProvider == null) {
+ lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
+ return false;
+ }
+
+ if (block instanceof TrappedChestBlock) {
+ bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED);
+ } else {
+ bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED);
+ }
+ }
+
+ if (block instanceof ShulkerBoxBlock) {
+ bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED);
+ }
+
+ if (block instanceof BarrelBlock) {
+ bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL);
+ }
+
+ // AnyChest only - SilentChest not active, container unsupported, or unnecessary.
+ if (!silentchest || player.gameMode.getGameModeForPlayer() == GameType.SPECTATOR) {
+ player.openMenu(menuProvider);
+ return true;
+ }
+
+ // SilentChest requires access to setting players' game mode directly.
+ if (this.serverPlayerGameModeGameType == null) {
+ return false;
+ }
+
+ if (blockEntity instanceof RandomizableContainerBlockEntity lootable) {
+ if (lootable.lootTable != null) {
+ lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
+ return false;
+ }
+ }
+
+ GameType gameType = player.gameMode.getGameModeForPlayer();
+ this.forceGameType(player, GameType.SPECTATOR);
+ player.openMenu(menuProvider);
+ this.forceGameType(player, gameType);
+ return true;
+ }
+
+ @Override
+ public void deactivateContainer(@NotNull final Player bukkitPlayer) {
+ if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) {
+ return;
+ }
+
+ ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
+
+ // Force game mode change without informing plugins or players.
+ // Regular game mode set calls GameModeChangeEvent and is cancellable.
+ GameType gameType = player.gameMode.getGameModeForPlayer();
+ this.forceGameType(player, GameType.SPECTATOR);
+
+ // ServerPlayer#closeContainer cannot be called without entering an
+ // infinite loop because this method is called during inventory close.
+ // From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent
+ player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity());
+ // From ServerPlayer#closeContainer
+ player.doCloseContainer();
+ // Regular inventory close will handle the rest - packet sending, etc.
+
+ // Revert forced game mode.
+ this.forceGameType(player, gameType);
+ }
+
+ private void forceGameType(final ServerPlayer player, final GameType gameMode) {
+ if (this.serverPlayerGameModeGameType == null) {
+ // No need to warn repeatedly, error on startup and lack of function should be enough.
+ return;
+ }
+ try {
+ this.serverPlayerGameModeGameType.setAccessible(true);
+ this.serverPlayerGameModeGameType.set(player.gameMode, gameMode);
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e);
+ }
+ }
+
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java
new file mode 100644
index 00000000..77aefe24
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java
@@ -0,0 +1,190 @@
+package com.lishid.openinv.internal.reobf.container.bukkit;
+
+import com.google.common.base.Preconditions;
+import com.lishid.openinv.internal.reobf.container.BaseOpenInventory;
+import net.minecraft.core.NonNullList;
+import net.minecraft.world.entity.player.Inventory;
+import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftInventory;
+import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.PlayerInventory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class OpenPlayerInventory extends CraftInventory implements PlayerInventory {
+
+ public OpenPlayerInventory(@NotNull BaseOpenInventory inventory) {
+ super(inventory);
+ }
+
+ @Override
+ public @NotNull BaseOpenInventory getInventory() {
+ return (BaseOpenInventory) super.getInventory();
+ }
+
+ @Override
+ public ItemStack @NotNull [] getContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents());
+ }
+
+ @Override
+ public void setContents(ItemStack[] items) {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ int size = internal.getContainerSize();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+
+ for (int index = 0; index < size; ++index) {
+ if (index < items.length) {
+ internal.setItem(index, CraftItemStack.asNMSCopy(items[index]));
+ } else {
+ internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY);
+ }
+ }
+ }
+
+ @Override
+ public ItemStack @NotNull [] getStorageContents() {
+ return asCraftMirror(getInventory().getOwnerHandle().getInventory().getNonEquipmentItems());
+ }
+
+ @Override
+ public void setStorageContents(ItemStack[] items) throws IllegalArgumentException {
+ NonNullList list = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems();
+ int size = list.size();
+ Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
+ for (int index = 0; index < items.length; ++index) {
+ list.set(index, CraftItemStack.asNMSCopy(items[index]));
+ }
+ }
+
+ @Override
+ public @NotNull InventoryType getType() {
+ return InventoryType.PLAYER;
+ }
+
+ @Override
+ public @NotNull Player getHolder() {
+ return getInventory().getOwner();
+ }
+
+ @Override
+ public @NotNull ItemStack @NotNull [] getArmorContents() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getArmorContents();
+ }
+
+ @Override
+ public void setArmorContents(ItemStack @NotNull [] items) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setArmorContents(items);
+ }
+
+ @Override
+ public @NotNull ItemStack @NotNull [] getExtraContents() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getExtraContents();
+ }
+
+ @Override
+ public void setExtraContents(ItemStack @NotNull [] items) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setExtraContents(items);
+ }
+
+ @Override
+ public @Nullable ItemStack getHelmet() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getHelmet();
+ }
+
+ @Override
+ public void setHelmet(@Nullable ItemStack helmet) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setHelmet(helmet);
+ }
+
+ @Override
+ public @Nullable ItemStack getChestplate() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getChestplate();
+ }
+
+ @Override
+ public void setChestplate(@Nullable ItemStack chestplate) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setChestplate(chestplate);
+ }
+
+ @Override
+ public @Nullable ItemStack getLeggings() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getLeggings();
+ }
+
+ @Override
+ public void setLeggings(@Nullable ItemStack leggings) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setLeggings(leggings);
+ }
+
+ @Override
+ public @Nullable ItemStack getBoots() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getBoots();
+ }
+
+ @Override
+ public void setBoots(@Nullable ItemStack boots) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setBoots(boots);
+ }
+
+ @Override
+ public @NotNull ItemStack getItemInMainHand() {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ return CraftItemStack.asCraftMirror(internal.getSelectedItem());
+ }
+
+ @Override
+ public void setItemInMainHand(@Nullable ItemStack item) {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ internal.setSelectedItem(CraftItemStack.asNMSCopy(item));
+ }
+
+ @Override
+ public @NotNull ItemStack getItemInOffHand() {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItemInOffHand();
+ }
+
+ @Override
+ public void setItemInOffHand(@Nullable ItemStack item) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItemInOffHand(item);
+ }
+
+ @SuppressWarnings("InlineMeSuggester")
+ @Deprecated
+ @Override
+ public @NotNull ItemStack getItemInHand() {
+ return getItemInMainHand();
+ }
+
+ @SuppressWarnings("InlineMeSuggester")
+ @Deprecated
+ @Override
+ public void setItemInHand(@Nullable ItemStack stack) {
+ setItemInMainHand(stack);
+ }
+
+ @Override
+ public int getHeldItemSlot() {
+ Inventory internal = getInventory().getOwnerHandle().getInventory();
+ return internal.getNonEquipmentItems().size() - 9 + internal.getSelectedSlot();
+ }
+
+ @Override
+ public void setHeldItemSlot(int slot) {
+ slot %= 9;
+ getInventory().getOwnerHandle().getInventory().setSelectedSlot(slot);
+ }
+
+ @Override
+ public @Nullable ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) {
+ return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItem(slot);
+ }
+
+ @Override
+ public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) {
+ getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItem(slot, item);
+ }
+
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java
new file mode 100644
index 00000000..bfec218e
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java
@@ -0,0 +1,116 @@
+package com.lishid.openinv.internal.reobf.container.slot;
+
+import com.lishid.openinv.internal.reobf.container.slot.placeholder.Placeholders;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.bukkit.craftbukkit.v1_21_R7.CraftEquipmentSlot;
+import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
+import org.bukkit.event.inventory.InventoryType;
+import org.bukkit.inventory.PlayerInventory;
+import org.jetbrains.annotations.NotNull;
+
+public class ContentEquipment implements Content {
+
+ private PlayerInventory equipment;
+ private final ItemStack placeholder;
+ private final org.bukkit.inventory.EquipmentSlot equipmentSlot;
+
+ public ContentEquipment(ServerPlayer holder, EquipmentSlot equipmentSlot) {
+ setHolder(holder);
+ placeholder = switch (equipmentSlot) {
+ case HEAD -> Placeholders.emptyHelmet;
+ case CHEST -> Placeholders.emptyChestplate;
+ case LEGS -> Placeholders.emptyLeggings;
+ case FEET -> Placeholders.emptyBoots;
+ default -> Placeholders.emptyOffHand;
+ };
+ this.equipmentSlot = CraftEquipmentSlot.getSlot(equipmentSlot);
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ this.equipment = holder.getBukkitEntity().getInventory();
+ }
+
+ @Override
+ public ItemStack get() {
+ return CraftItemStack.asNMSCopy(equipment.getItem(equipmentSlot));
+ }
+
+ @Override
+ public ItemStack remove() {
+ org.bukkit.inventory.ItemStack old = equipment.getItem(equipmentSlot);
+ equipment.setItem(equipmentSlot, null);
+ return CraftItemStack.asNMSCopy(old);
+ }
+
+ @Override
+ public ItemStack removePartial(int amount) {
+ if (amount <= 0) {
+ return ItemStack.EMPTY;
+ }
+ ItemStack current = get();
+ if (current.isEmpty()) {
+ return ItemStack.EMPTY;
+ }
+ ItemStack split = current.split(amount);
+ set(current);
+ return split;
+ }
+
+ @Override
+ public void set(ItemStack itemStack) {
+ equipment.setItem(equipmentSlot, CraftItemStack.asCraftMirror(itemStack));
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y);
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.ARMOR;
+ }
+
+ public class SlotEquipment extends SlotPlaceholder {
+
+ private ServerPlayer viewer;
+
+ SlotEquipment(Container container, int index, int x, int y) {
+ super(container, index, x, y);
+ }
+
+ @Override
+ public ItemStack getOrDefault() {
+ ItemStack itemStack = getItem();
+ if (!itemStack.isEmpty()) {
+ return itemStack;
+ }
+ return placeholder;
+ }
+
+ public EquipmentSlot getEquipmentSlot() {
+ return CraftEquipmentSlot.getNMS(equipmentSlot);
+ }
+
+ public void onlyEquipmentFor(ServerPlayer viewer) {
+ this.viewer = viewer;
+ }
+
+ @Override
+ public boolean mayPlace(@NotNull ItemStack itemStack) {
+ if (viewer == null) {
+ return true;
+ }
+
+ return equipmentSlot == org.bukkit.inventory.EquipmentSlot.OFF_HAND
+ || viewer.getEquipmentSlotForItem(itemStack) == CraftEquipmentSlot.getNMS(equipmentSlot);
+ }
+
+ }
+
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java
new file mode 100644
index 00000000..5052987a
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java
@@ -0,0 +1,50 @@
+package com.lishid.openinv.internal.reobf.container.slot;
+
+import com.lishid.openinv.internal.reobf.player.OpenPlayer;
+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.Container;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.inventory.InventoryMenu;
+import net.minecraft.world.inventory.Slot;
+import org.bukkit.event.inventory.InventoryType;
+import org.jetbrains.annotations.NotNull;
+
+public class ContentOffHand extends ContentEquipment {
+
+ private ServerPlayer holder;
+
+ public ContentOffHand(ServerPlayer holder) {
+ super(holder, EquipmentSlot.OFFHAND);
+ }
+
+ @Override
+ public void setHolder(@NotNull ServerPlayer holder) {
+ super.setHolder(holder);
+ this.holder = holder;
+ }
+
+ @Override
+ public InventoryType.SlotType getSlotType() {
+ return InventoryType.SlotType.QUICKBAR;
+ }
+
+ @Override
+ public Slot asSlot(Container container, int slot, int x, int y) {
+ return new SlotEquipment(container, slot, x, y) {
+ @Override
+ public void setChanged() {
+ if (OpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) {
+ holder.connection.send(
+ new ClientboundContainerSetSlotPacket(
+ holder.inventoryMenu.containerId,
+ holder.inventoryMenu.incrementStateId(),
+ InventoryMenu.SHIELD_SLOT,
+ holder.getOffhandItem()
+ ));
+ }
+ }
+ };
+ }
+
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java
new file mode 100644
index 00000000..33c630ac
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java
@@ -0,0 +1,216 @@
+package com.lishid.openinv.internal.reobf.player;
+
+import com.lishid.openinv.event.OpenEvents;
+import com.mojang.logging.LogUtils;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtIo;
+import net.minecraft.nbt.NumericTag;
+import net.minecraft.nbt.Tag;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.server.network.ServerGamePacketListenerImpl;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.util.Util;
+import net.minecraft.world.level.storage.PlayerDataStorage;
+import net.minecraft.world.level.storage.TagValueOutput;
+import net.minecraft.world.level.storage.ValueOutput;
+import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
+import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+import org.slf4j.Logger;
+
+import java.lang.reflect.Field;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+public class OpenPlayer extends CraftPlayer {
+
+ /**
+ * List of tags to always reset when saving.
+ *
+ * @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput)
+ * @see ServerPlayer#addAdditionalSaveData(ValueOutput)
+ * @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(ValueOutput)
+ * @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(ValueOutput)
+ */
+ @Unmodifiable
+ protected static final Set RESET_TAGS = Set.of(
+ // Entity#saveWithoutId(CompoundTag)
+ "CustomName",
+ "CustomNameVisible",
+ "Silent",
+ "NoGravity",
+ "Glowing",
+ "TicksFrozen",
+ "HasVisualFire",
+ "Tags",
+ "Passengers",
+ // ServerPlayer#addAdditionalSaveData(CompoundTag)
+ // Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
+ "warden_spawn_tracker",
+ "entered_nether_pos", // Replaces enteredNetherPosition
+ "enteredNetherPosition",
+ "respawn", // Replaces SpawnXyz fields as of 1.21.6
+ "SpawnX",
+ "SpawnY",
+ "SpawnZ",
+ "SpawnForced",
+ "SpawnAngle",
+ "SpawnDimension",
+ "raid_omen_position",
+ "ender_pearls",
+ // Player#addAdditionalSaveData(CompoundTag)
+ "ShoulderEntityLeft",
+ "ShoulderEntityRight",
+ "LastDeathLocation",
+ "current_explosion_impact_pos",
+ // LivingEntity#addAdditionalSaveData(CompoundTag)
+ "active_effects",
+ "sleeping_pos", // Replaces SleepingXyz fields as of 1.21.6
+ "SleepingX",
+ "SleepingY",
+ "SleepingZ",
+ "Brain",
+ "last_hurt_by_player",
+ "last_hurt_by_player_memory_time",
+ "last_hurt_by_mob",
+ "ticks_since_last_hurt_by_mob",
+ "equipment",
+ "locator_bar_icon"
+ );
+
+ private final PlayerManager manager;
+
+ protected OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
+ super(server, entity);
+ this.manager = manager;
+ }
+
+ @Override
+ public void loadData() {
+ manager.loadData(getHandle());
+ }
+
+ @Override
+ public void saveData() {
+ if (OpenEvents.saveCancelled(this)) {
+ return;
+ }
+
+ ServerPlayer player = this.getHandle();
+ Logger logger = LogUtils.getLogger();
+ // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
+ PlayerDataStorage worldNBTStorage = server.getServer().getPlayerList().playerIo;
+
+ CompoundTag oldData = isOnline()
+ ? null
+ : worldNBTStorage.load(player.nameAndId()).orElse(null);
+ CompoundTag playerData = getWritableTag(oldData);
+
+ ValueOutput valueOutput = TagValueOutput.createWithContext(scopedCollector, player.registryAccess());
+ Field tagValueOutputOutput = TagValueOutput.class.getDeclaredField("output");
+ tagValueOutputOutput.setAccessible(true);
+ CompoundTag newPlayerData = (CompoundTag) tagValueOutputOutput.get(valueOutput);
+ // Add existing old data.
+ newPlayerData.merge(playerData);
+ playerData = newPlayerData;
+
+ player.saveWithoutId(valueOutput);
+
+ if (oldData != null) {
+ // Revert certain special data values when offline.
+ revertSpecialValues(playerData, oldData);
+ }
+
+ Path playerDataDir = worldNBTStorage.getPlayerDir().toPath();
+ Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
+ NbtIo.writeCompressed(playerData, tempFile);
+ Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
+ Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
+ Util.safeReplaceFile(dataFile, tempFile, backupFile);
+ } catch (Exception e) {
+ LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
+ }
+ }
+
+ @Contract("null -> new")
+ protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
+ if (oldData == null) {
+ return new CompoundTag();
+ }
+
+ // Copy old data. This is a deep clone, so operating on it should be safe.
+ oldData = oldData.copy();
+
+ // Remove vanilla/server data that is not written every time.
+ oldData.keySet().removeIf(
+ key -> RESET_TAGS.contains(key)
+ || key.startsWith("Bukkit")
+ || (key.startsWith("Paper") && key.length() > 5)
+ );
+
+ return oldData;
+ }
+
+ protected void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) {
+ // Revert automatic updates to play timestamps.
+ copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
+ copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
+ copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class);
+ }
+
+ private void copyValue(
+ @NotNull CompoundTag source,
+ @NotNull CompoundTag target,
+ @NotNull String container,
+ @NotNull String key,
+ @SuppressWarnings("SameParameterValue") @NotNull Class tagType
+ ) {
+ CompoundTag oldContainer = getTag(source, container, CompoundTag.class);
+ CompoundTag newContainer = getTag(target, container, CompoundTag.class);
+
+ // New container being null means the server implementation doesn't store this data.
+ if (newContainer == null) {
+ return;
+ }
+
+ // If old tag exists, copy it to new location, removing otherwise.
+ setTag(newContainer, key, getTag(oldContainer, key, tagType));
+ }
+
+ private @Nullable T getTag(
+ @Nullable CompoundTag container,
+ @NotNull String key,
+ @NotNull Class dataType
+ ) {
+ if (container == null) {
+ return null;
+ }
+ Tag value = container.get(key);
+ if (value == null || !dataType.isAssignableFrom(value.getClass())) {
+ return null;
+ }
+ return dataType.cast(value);
+ }
+
+ private void setTag(
+ @NotNull CompoundTag container,
+ @NotNull String key,
+ @Nullable T data
+ ) {
+ if (data == null) {
+ container.remove(key);
+ } else {
+ container.put(key, data);
+ }
+ }
+
+ public static boolean isConnected(@Nullable ServerGamePacketListenerImpl connection) {
+ return connection != null && !connection.isDisconnected();
+ }
+
+}
diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java
new file mode 100644
index 00000000..d619f847
--- /dev/null
+++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java
@@ -0,0 +1,237 @@
+package com.lishid.openinv.internal.reobf.player;
+
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.internal.reobf.container.OpenEnderChest;
+import com.lishid.openinv.internal.reobf.container.OpenInventory;
+import com.lishid.openinv.internal.reobf.container.menu.OpenChestMenu;
+import com.lishid.openinv.util.JulLoggerAdapter;
+import com.mojang.authlib.GameProfile;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ClientInformation;
+import net.minecraft.server.level.ParticleStatus;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.ProblemReporter;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.ChatVisiblity;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.storage.TagValueInput;
+import net.minecraft.world.level.storage.ValueInput;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.Server;
+import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
+import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
+import org.bukkit.craftbukkit.v1_21_R7.event.CraftEventFactory;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Field;
+import java.util.logging.Logger;
+
+public class PlayerManager implements com.lishid.openinv.internal.PlayerManager {
+
+ private final @NotNull Logger logger;
+ private @Nullable Field bukkitEntity;
+
+ public PlayerManager(@NotNull Logger logger) {
+ this.logger = logger;
+ try {
+ bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
+ } catch (NoSuchFieldException e) {
+ logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!");
+ logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
+ bukkitEntity = null;
+ }
+ }
+
+ public static @NotNull ServerPlayer getHandle(final Player player) {
+ if (player instanceof CraftPlayer craftPlayer) {
+ return craftPlayer.getHandle();
+ }
+
+ Server server = player.getServer();
+ ServerPlayer nmsPlayer = null;
+
+ if (server instanceof CraftServer craftServer) {
+ nmsPlayer = craftServer.getHandle().getPlayer(player.getUniqueId());
+ }
+
+ if (nmsPlayer == null) {
+ // Could use reflection to examine fields, but it's honestly not worth the bother.
+ throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName());
+ }
+
+ return nmsPlayer;
+ }
+
+ @Override
+ public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
+ if (!(Bukkit.getServer() instanceof CraftServer craftServer)) {
+ return null;
+ }
+
+ MinecraftServer server = craftServer.getServer();
+ ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
+
+ if (worldServer == null) {
+ return null;
+ }
+
+ // Create a new ServerPlayer.
+ ServerPlayer entity = createNewPlayer(server, worldServer, offline);
+
+ // Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak.
+ entity.getAdvancements().stopListening();
+
+ // Try to load the player's data.
+ if (loadData(entity)) {
+ // If data is loaded successfully, return the Bukkit entity.
+ return entity.getBukkitEntity();
+ }
+
+ return null;
+ }
+
+ private @NotNull ServerPlayer createNewPlayer(
+ @NotNull MinecraftServer server,
+ @NotNull ServerLevel worldServer,
+ @NotNull final OfflinePlayer offline
+ ) {
+ // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
+ // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
+ GameProfile profile = new GameProfile(offline.getUniqueId(),
+ offline.getName() != null ? offline.getName() : offline.getUniqueId().toString()
+ );
+
+ ClientInformation dummyInfo = new ClientInformation(
+ "en_us",
+ 1, // Reduce distance just in case.
+ ChatVisiblity.HIDDEN, // Don't accept chat.
+ false,
+ ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
+ ServerPlayer.DEFAULT_MAIN_HAND,
+ true,
+ false, // Don't list in player list (not that this player is in the list anyway).
+ ParticleStatus.MINIMAL
+ );
+
+ ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
+
+ try {
+ injectPlayer(entity);
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ }
+
+ return entity;
+ }
+
+ boolean loadData(@NotNull ServerPlayer player) {
+ // See CraftPlayer#loadData
+
+ try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
+ CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null);
+
+ if (loadedData == null) {
+ // Exceptions with loading are logged.
+ return false;
+ }
+
+ ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData);
+
+ // Read basic data into the player.
+ player.load(valueInput);
+ }
+
+ return true;
+ }
+
+ private void injectPlayer(ServerPlayer player) throws IllegalAccessException {
+ if (bukkitEntity == null) {
+ return;
+ }
+
+ bukkitEntity.setAccessible(true);
+
+ bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
+ }
+
+ @Override
+ public @NotNull Player inject(@NotNull Player player) {
+ try {
+ ServerPlayer nmsPlayer = getHandle(player);
+ if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) {
+ return openPlayer;
+ }
+ injectPlayer(nmsPlayer);
+ return nmsPlayer.getBukkitEntity();
+ } catch (IllegalAccessException e) {
+ logger.log(
+ java.util.logging.Level.WARNING,
+ e,
+ () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
+ );
+ return player;
+ }
+ }
+
+ @Override
+ public @Nullable InventoryView openInventory(
+ @NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory,
+ boolean viewOnly
+ ) {
+ ServerPlayer player = getHandle(bukkitPlayer);
+
+ if (!OpenPlayer.isConnected(player.connection)) {
+ return null;
+ }
+
+ // See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
+ OpenChestMenu> menu;
+ Component title;
+ if (inventory instanceof OpenInventory playerInv) {
+ menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly);
+ title = playerInv.getTitle(player, menu);
+ } else if (inventory instanceof OpenEnderChest enderChest) {
+ menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly);
+ title = enderChest.getTitle(menu);
+ } else {
+ return null;
+ }
+
+ // Should never happen, player is a ServerPlayer with an active connection.
+ if (menu == null) {
+ return null;
+ }
+
+ // Set up title. Title can only be set once for a menu, and is set during the open process.
+ // Further title changes are a hack where the client is sent a "new" inventory with the same ID,
+ // resulting in a title change but no other state modifications (like cursor position).
+ menu.setTitle(title);
+
+ AbstractContainerMenu opened = CraftEventFactory.callInventoryOpenEvent(player, menu, false);
+
+ // Menu is null if event is cancelled.
+ if (opened == null) {
+ return null;
+ }
+
+ player.containerMenu = opened;
+ player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), opened.getTitle()));
+ player.initMenu(opened);
+
+ return opened.getBukkitView();
+ }
+
+}
diff --git a/internal/v1_16_R3/pom.xml b/internal/v1_16_R3/pom.xml
deleted file mode 100644
index 74905edd..00000000
--- a/internal/v1_16_R3/pom.xml
+++ /dev/null
@@ -1,75 +0,0 @@
-
-
-
-
- 4.0.0
-
-
- com.lishid
- openinvinternal
- 4.1.6-SNAPSHOT
-
-
- openinvadapter1_16_R3
- OpenInvAdapter1_16_R3
-
-
-
- org.spigotmc
- spigot
- 1.16.5-R0.1-SNAPSHOT
- provided
-
-
- com.lishid
- openinvplugincore
- 4.1.6-SNAPSHOT
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.2.2
-
- true
-
-
-
- package
-
- shade
-
-
-
-
-
-
- maven-compiler-plugin
- 3.8.1
-
- 1.8
- 1.8
-
-
-
-
-
-
diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java
deleted file mode 100644
index 5d78617d..00000000
--- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal.v1_16_R3;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.internal.IAnySilentContainer;
-import java.lang.reflect.Field;
-import net.minecraft.server.v1_16_R3.Block;
-import net.minecraft.server.v1_16_R3.BlockBarrel;
-import net.minecraft.server.v1_16_R3.BlockChest;
-import net.minecraft.server.v1_16_R3.BlockChestTrapped;
-import net.minecraft.server.v1_16_R3.BlockPosition;
-import net.minecraft.server.v1_16_R3.BlockPropertyChestType;
-import net.minecraft.server.v1_16_R3.BlockShulkerBox;
-import net.minecraft.server.v1_16_R3.ChatMessage;
-import net.minecraft.server.v1_16_R3.Container;
-import net.minecraft.server.v1_16_R3.ContainerChest;
-import net.minecraft.server.v1_16_R3.Containers;
-import net.minecraft.server.v1_16_R3.EntityHuman;
-import net.minecraft.server.v1_16_R3.EntityPlayer;
-import net.minecraft.server.v1_16_R3.EnumGamemode;
-import net.minecraft.server.v1_16_R3.IBlockData;
-import net.minecraft.server.v1_16_R3.IChatBaseComponent;
-import net.minecraft.server.v1_16_R3.ITileInventory;
-import net.minecraft.server.v1_16_R3.InventoryEnderChest;
-import net.minecraft.server.v1_16_R3.InventoryLargeChest;
-import net.minecraft.server.v1_16_R3.PlayerInteractManager;
-import net.minecraft.server.v1_16_R3.PlayerInventory;
-import net.minecraft.server.v1_16_R3.TileEntity;
-import net.minecraft.server.v1_16_R3.TileEntityChest;
-import net.minecraft.server.v1_16_R3.TileEntityEnderChest;
-import net.minecraft.server.v1_16_R3.TileEntityLootable;
-import net.minecraft.server.v1_16_R3.TileInventory;
-import net.minecraft.server.v1_16_R3.World;
-import org.bukkit.Material;
-import org.bukkit.Statistic;
-import org.bukkit.block.Barrel;
-import org.bukkit.block.BlockFace;
-import org.bukkit.block.BlockState;
-import org.bukkit.block.EnderChest;
-import org.bukkit.block.ShulkerBox;
-import org.bukkit.block.data.BlockData;
-import org.bukkit.block.data.Directional;
-import org.bukkit.block.data.type.Chest;
-import org.bukkit.entity.Cat;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryView;
-import org.bukkit.util.BoundingBox;
-import org.jetbrains.annotations.NotNull;
-
-public class AnySilentContainer implements IAnySilentContainer {
-
- private Field playerInteractManagerGamemode;
-
- public AnySilentContainer() {
- try {
- this.playerInteractManagerGamemode = PlayerInteractManager.class.getDeclaredField("gamemode");
- this.playerInteractManagerGamemode.setAccessible(true);
- } catch (NoSuchFieldException | SecurityException e) {
- System.err.println("[OpenInv] Unable to directly write player gamemode! SilentChest will fail.");
- e.printStackTrace();
- }
- }
-
- @Override
- public boolean isAnySilentContainer(@NotNull final org.bukkit.block.Block bukkitBlock) {
- if (bukkitBlock.getType() == Material.ENDER_CHEST) {
- return true;
- }
- BlockState state = bukkitBlock.getState();
- return state instanceof org.bukkit.block.Chest
- || state instanceof org.bukkit.block.ShulkerBox
- || state instanceof org.bukkit.block.Barrel;
- }
-
- @Override
- public boolean isAnyContainerNeeded(@NotNull final Player p, @NotNull final org.bukkit.block.Block block) {
- BlockState blockState = block.getState();
-
- // Barrels do not require AnyContainer.
- if (blockState instanceof Barrel) {
- return false;
- }
-
- // Enderchests require a non-occluding block on top to open.
- if (blockState instanceof EnderChest) {
- return block.getRelative(0, 1, 0).getType().isOccluding();
- }
-
- // Shulker boxes require 1/2 a block clear in the direction they open.
- if (blockState instanceof ShulkerBox) {
- BoundingBox boundingBox = block.getBoundingBox();
- if (boundingBox.getVolume() > 1) {
- // Shulker box is already open.
- return false;
- }
-
- BlockData blockData = block.getBlockData();
- if (!(blockData instanceof Directional)) {
- // Shouldn't be possible. Just in case, demand AnyChest.
- return true;
- }
-
- Directional directional = (Directional) blockData;
- BlockFace face = directional.getFacing();
- boundingBox.shift(face.getDirection());
- // Return whether or not bounding boxes overlap.
- return block.getRelative(face, 1).getBoundingBox().overlaps(boundingBox);
- }
-
- if (!(blockState instanceof org.bukkit.block.Chest)) {
- return false;
- }
-
- if (isBlockedChest(block)) {
- return true;
- }
-
- BlockData blockData = block.getBlockData();
- if (!(blockData instanceof Chest) || ((Chest) blockData).getType() == Chest.Type.SINGLE) {
- return false;
- }
-
- Chest chest = (Chest) blockData;
- int ordinal = (chest.getFacing().ordinal() + 4 + (chest.getType() == Chest.Type.RIGHT ? -1 : 1)) % 4;
- BlockFace relativeFace = BlockFace.values()[ordinal];
- org.bukkit.block.Block relative = block.getRelative(relativeFace);
-
- if (relative.getType() != block.getType()) {
- return false;
- }
-
- BlockData relativeData = relative.getBlockData();
- if (!(relativeData instanceof Chest)) {
- return false;
- }
-
- Chest relativeChest = (Chest) relativeData;
- if (relativeChest.getFacing() != chest.getFacing()
- || relativeChest.getType() != (chest.getType() == Chest.Type.RIGHT ? Chest.Type.LEFT : Chest.Type.RIGHT)) {
- return false;
- }
-
- return isBlockedChest(relative);
- }
-
- private boolean isBlockedChest(org.bukkit.block.Block block) {
- org.bukkit.block.Block relative = block.getRelative(0, 1, 0);
- return relative.getType().isOccluding()
- || block.getWorld().getNearbyEntities(BoundingBox.of(relative), entity -> entity instanceof Cat).size() > 0;
- }
-
- @Override
- public boolean activateContainer(@NotNull final Player bukkitPlayer, final boolean silentchest,
- @NotNull final org.bukkit.block.Block bukkitBlock) {
-
- // Silent ender chest is API-only
- if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) {
- bukkitPlayer.openInventory(bukkitPlayer.getEnderChest());
- bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
- return true;
- }
-
- EntityPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
-
- final World world = player.world;
- final BlockPosition blockPosition = new BlockPosition(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ());
- final TileEntity tile = world.getTileEntity(blockPosition);
-
- if (tile == null) {
- return false;
- }
-
- if (tile instanceof TileEntityEnderChest) {
- // Anychest ender chest. See net.minecraft.server.BlockEnderChest
- InventoryEnderChest enderChest = player.getEnderChest();
- enderChest.a((TileEntityEnderChest) tile);
- player.openContainer(new TileInventory((containerCounter, playerInventory, ignored) -> {
- Containers> containers = PlayerDataManager.getContainers(enderChest.getSize());
- int rows = enderChest.getSize() / 9;
- return new ContainerChest(containers, containerCounter, playerInventory, enderChest, rows);
- }, new ChatMessage("container.enderchest")));
- bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
- return true;
- }
-
- if (!(tile instanceof ITileInventory)) {
- return false;
- }
-
- ITileInventory tileInventory = (ITileInventory) tile;
- IBlockData blockData = world.getType(blockPosition);
- Block block = blockData.getBlock();
-
- if (block instanceof BlockChest) {
-
- BlockPropertyChestType chestType = blockData.get(BlockChest.c);
-
- if (chestType != BlockPropertyChestType.SINGLE) {
-
- BlockPosition adjacentBlockPosition = blockPosition.shift(BlockChest.h(blockData));
- IBlockData adjacentBlockData = world.getType(adjacentBlockPosition);
-
- if (adjacentBlockData.getBlock() == block) {
-
- BlockPropertyChestType adjacentChestType = adjacentBlockData.get(BlockChest.c);
-
- if (adjacentChestType != BlockPropertyChestType.SINGLE && chestType != adjacentChestType
- && adjacentBlockData.get(BlockChest.FACING) == blockData.get(BlockChest.FACING)) {
-
- TileEntity adjacentTile = world.getTileEntity(adjacentBlockPosition);
-
- if (adjacentTile instanceof TileEntityChest && tileInventory instanceof TileEntityChest) {
- TileEntityChest rightChest = chestType == BlockPropertyChestType.RIGHT ? ((TileEntityChest) tileInventory) : (TileEntityChest) adjacentTile;
- TileEntityChest leftChest = chestType == BlockPropertyChestType.RIGHT ? (TileEntityChest) adjacentTile : ((TileEntityChest) tileInventory);
-
- if (silentchest && (rightChest.lootTable != null || leftChest.lootTable != null)) {
- OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
- return false;
- }
-
- tileInventory = new ITileInventory() {
- public Container createMenu(int containerCounter, PlayerInventory playerInventory, EntityHuman entityHuman) {
- leftChest.d(playerInventory.player);
- rightChest.d(playerInventory.player);
- return ContainerChest.b(containerCounter, playerInventory, new InventoryLargeChest(rightChest, leftChest));
- }
-
- public IChatBaseComponent getScoreboardDisplayName() {
- if (leftChest.hasCustomName()) {
- return leftChest.getScoreboardDisplayName();
- }
- if (rightChest.hasCustomName()) {
- return rightChest.getScoreboardDisplayName();
- }
- return new ChatMessage("container.chestDouble");
- }
- };
- }
- }
- }
- }
-
- if (block instanceof BlockChestTrapped) {
- bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED);
- } else {
- bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED);
- }
- }
-
- if (block instanceof BlockShulkerBox) {
- bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED);
- }
-
- if (block instanceof BlockBarrel) {
- bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL);
- }
-
- // AnyChest only - SilentChest not active, container unsupported, or unnecessary.
- if (!silentchest || player.playerInteractManager.getGameMode() == EnumGamemode.SPECTATOR) {
- player.openContainer(tileInventory);
- return true;
- }
-
- // SilentChest requires access to setting players' gamemode directly.
- if (this.playerInteractManagerGamemode == null) {
- return false;
- }
-
- if (tile instanceof TileEntityLootable) {
- TileEntityLootable lootable = (TileEntityLootable) tile;
- if (lootable.lootTable != null) {
- OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
- return false;
- }
- }
-
- EnumGamemode gamemode = player.playerInteractManager.getGameMode();
- this.forceGameMode(player, EnumGamemode.SPECTATOR);
- player.openContainer(tileInventory);
- this.forceGameMode(player, gamemode);
- return true;
- }
-
- @Override
- public void deactivateContainer(@NotNull final Player bukkitPlayer) {
- if (this.playerInteractManagerGamemode == null) {
- return;
- }
-
- InventoryView view = bukkitPlayer.getOpenInventory();
- switch (view.getType()) {
- case CHEST:
- case ENDER_CHEST:
- case SHULKER_BOX:
- case BARREL:
- break;
- default:
- return;
- }
-
- EntityPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
-
- EnumGamemode gamemode = player.playerInteractManager.getGameMode();
- this.forceGameMode(player, EnumGamemode.SPECTATOR);
- player.activeContainer.b(player);
- player.activeContainer.a(player, false);
- player.activeContainer.transferTo(player.defaultContainer, player.getBukkitEntity());
- player.activeContainer = player.defaultContainer;
- this.forceGameMode(player, gamemode);
- }
-
- private void forceGameMode(final EntityPlayer player, final EnumGamemode gameMode) {
- if (this.playerInteractManagerGamemode == null) {
- // No need to warn repeatedly, error on startup and lack of function should be enough.
- return;
- }
- try {
- if (!this.playerInteractManagerGamemode.isAccessible()) {
- // Just in case, ensure accessible.
- this.playerInteractManagerGamemode.setAccessible(true);
- }
- this.playerInteractManagerGamemode.set(player.playerInteractManager, gameMode);
- } catch (IllegalArgumentException | IllegalAccessException e) {
- e.printStackTrace();
- }
- }
-
-}
diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java
deleted file mode 100644
index def3f960..00000000
--- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java
+++ /dev/null
@@ -1,69 +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.internal.v1_16_R3;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import net.minecraft.server.v1_16_R3.EntityPlayer;
-import net.minecraft.server.v1_16_R3.NBTCompressedStreamTools;
-import net.minecraft.server.v1_16_R3.NBTTagCompound;
-import net.minecraft.server.v1_16_R3.WorldNBTStorage;
-import org.apache.logging.log4j.LogManager;
-import org.bukkit.craftbukkit.v1_16_R3.CraftServer;
-import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer;
-
-public class OpenPlayer extends CraftPlayer {
-
- public OpenPlayer(CraftServer server, EntityPlayer entity) {
- super(server, entity);
- }
-
- @Override
- public void saveData() {
- super.saveData();
- EntityPlayer player = this.getHandle();
- // See net.minecraft.server.WorldNBTStorage#save(EntityPlayer)
- try {
- WorldNBTStorage worldNBTStorage = player.server.getPlayerList().playerFileData;
-
- NBTTagCompound playerData = player.save(new NBTTagCompound());
-
- if (!isOnline()) {
- // Special case: save old vehicle data
- NBTTagCompound oldData = worldNBTStorage.load(player);
-
- if (oldData != null && oldData.hasKeyOfType("RootVehicle", 10)) {
- // See net.minecraft.server.PlayerList#a(NetworkManager, EntityPlayer) and net.minecraft.server.EntityPlayer#b(NBTTagCompound)
- playerData.set("RootVehicle", oldData.getCompound("RootVehicle"));
- }
- }
-
- File file = new File(worldNBTStorage.getPlayerDir(), player.getUniqueIDString() + ".dat.tmp");
- File file1 = new File(worldNBTStorage.getPlayerDir(), player.getUniqueIDString() + ".dat");
-
- NBTCompressedStreamTools.a(playerData, new FileOutputStream(file));
-
- if (file1.exists() && !file1.delete() || !file.renameTo(file1)) {
- LogManager.getLogger().warn("Failed to save player data for {}", player.getDisplayName().getString());
- }
-
- } catch (Exception e) {
- LogManager.getLogger().warn("Failed to save player data for {}", player.getDisplayName().getString());
- }
- }
-
-}
diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java
deleted file mode 100644
index 5a29ceb5..00000000
--- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal.v1_16_R3;
-
-import com.lishid.openinv.internal.IPlayerDataManager;
-import com.lishid.openinv.internal.ISpecialInventory;
-import com.lishid.openinv.internal.OpenInventoryView;
-import com.mojang.authlib.GameProfile;
-import java.lang.reflect.Field;
-import net.minecraft.server.v1_16_R3.ChatComponentText;
-import net.minecraft.server.v1_16_R3.Container;
-import net.minecraft.server.v1_16_R3.Containers;
-import net.minecraft.server.v1_16_R3.Entity;
-import net.minecraft.server.v1_16_R3.EntityPlayer;
-import net.minecraft.server.v1_16_R3.MinecraftServer;
-import net.minecraft.server.v1_16_R3.PacketPlayOutOpenWindow;
-import net.minecraft.server.v1_16_R3.PlayerInteractManager;
-import net.minecraft.server.v1_16_R3.World;
-import net.minecraft.server.v1_16_R3.WorldServer;
-import org.bukkit.Bukkit;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.Server;
-import org.bukkit.craftbukkit.v1_16_R3.CraftServer;
-import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer;
-import org.bukkit.craftbukkit.v1_16_R3.event.CraftEventFactory;
-import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftContainer;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryView;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public class PlayerDataManager implements IPlayerDataManager {
-
- private @Nullable Field bukkitEntity;
-
- public PlayerDataManager() {
- try {
- bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
- } catch (NoSuchFieldException e) {
- System.out.println("Unable to obtain field to inject custom save process - players' mounts may be deleted when loaded.");
- e.printStackTrace();
- bukkitEntity = null;
- }
- }
-
- @NotNull
- public static EntityPlayer getHandle(final Player player) {
- if (player instanceof CraftPlayer) {
- return ((CraftPlayer) player).getHandle();
- }
-
- Server server = player.getServer();
- EntityPlayer nmsPlayer = null;
-
- if (server instanceof CraftServer) {
- nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getName());
- }
-
- if (nmsPlayer == null) {
- // Could use reflection to examine fields, but it's honestly not worth the bother.
- throw new RuntimeException("Unable to fetch EntityPlayer from provided Player implementation");
- }
-
- return nmsPlayer;
- }
-
- @Nullable
- @Override
- public Player loadPlayer(@NotNull final OfflinePlayer offline) {
- // Ensure player has data
- if (!offline.hasPlayedBefore()) {
- return null;
- }
-
- // Create a profile and entity to load the player data
- // See net.minecraft.server.PlayerList#attemptLogin
- GameProfile profile = new GameProfile(offline.getUniqueId(),
- offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
- MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
- WorldServer worldServer = server.getWorldServer(World.OVERWORLD);
-
- if (worldServer == null) {
- return null;
- }
-
- EntityPlayer entity = new EntityPlayer(server, worldServer, profile, new PlayerInteractManager(worldServer));
-
- try {
- injectPlayer(entity);
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
-
- // Get the bukkit entity
- Player target = entity.getBukkitEntity();
- if (target != null) {
- // Load data
- target.loadData();
- }
- // Return the entity
- return target;
- }
-
- void injectPlayer(EntityPlayer player) throws IllegalAccessException {
- if (bukkitEntity == null) {
- return;
- }
-
- bukkitEntity.setAccessible(true);
-
- bukkitEntity.set(player, new OpenPlayer(player.server.server, player));
- }
-
- @NotNull
- @Override
- public Player inject(@NotNull Player player) {
- try {
- EntityPlayer nmsPlayer = getHandle(player);
- injectPlayer(nmsPlayer);
- return nmsPlayer.getBukkitEntity();
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- return player;
- }
- }
-
- @Nullable
- @Override
- public InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
-
- EntityPlayer nmsPlayer = getHandle(player);
-
- if (nmsPlayer.playerConnection == null) {
- return null;
- }
-
- InventoryView view = getView(player, inventory);
-
- if (view == null) {
- return player.openInventory(inventory.getBukkitInventory());
- }
-
- Container container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) {
- @Override
- public Containers> getType() {
- return getContainers(inventory.getBukkitInventory().getSize());
- }
- };
-
- container.setTitle(new ChatComponentText(view.getTitle()));
- container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container);
-
- if (container == null) {
- return null;
- }
-
- nmsPlayer.playerConnection.sendPacket(new PacketPlayOutOpenWindow(container.windowId, container.getType(),
- new ChatComponentText(container.getBukkitView().getTitle())));
- nmsPlayer.activeContainer = container;
- container.addSlotListener(nmsPlayer);
-
- return container.getBukkitView();
-
- }
-
- private @Nullable InventoryView getView(Player player, ISpecialInventory inventory) {
- if (inventory instanceof SpecialEnderChest) {
- return new OpenInventoryView(player, inventory, "container.enderchest", "'s Ender Chest");
- } else if (inventory instanceof SpecialPlayerInventory) {
- return new OpenInventoryView(player, inventory, "container.player", "'s Inventory");
- } else {
- return null;
- }
- }
-
- static @NotNull Containers> getContainers(int inventorySize) {
- switch (inventorySize) {
- case 9:
- return Containers.GENERIC_9X1;
- case 18:
- return Containers.GENERIC_9X2;
- case 36:
- return Containers.GENERIC_9X4;
- case 41: // PLAYER
- case 45:
- return Containers.GENERIC_9X5;
- case 54:
- return Containers.GENERIC_9X6;
- case 27:
- default:
- return Containers.GENERIC_9X3;
- }
- }
-
- @Override
- public int convertToPlayerSlot(InventoryView view, int rawSlot) {
- int topSize = view.getTopInventory().getSize();
- if (topSize <= rawSlot) {
- // Slot is not inside special inventory, use Bukkit logic.
- return view.convertSlot(rawSlot);
- }
-
- // Main inventory, slots 0-26 -> 9-35
- if (rawSlot < 27) {
- return rawSlot + 9;
- }
- // Hotbar, slots 27-35 -> 0-8
- if (rawSlot < 36) {
- return rawSlot - 27;
- }
- // Armor, slots 36-39 -> 39-36
- if (rawSlot < 40) {
- return 36 + (39 - rawSlot);
- }
- // Off hand
- if (rawSlot == 40) {
- return 40;
- }
- // Drop slots, "out of inventory"
- return -1;
- }
-
-}
diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java
deleted file mode 100644
index 7fe8beea..00000000
--- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal.v1_16_R3;
-
-import com.lishid.openinv.internal.ISpecialEnderChest;
-import java.util.List;
-import net.minecraft.server.v1_16_R3.AutoRecipeStackManager;
-import net.minecraft.server.v1_16_R3.ContainerUtil;
-import net.minecraft.server.v1_16_R3.EntityHuman;
-import net.minecraft.server.v1_16_R3.EntityPlayer;
-import net.minecraft.server.v1_16_R3.IInventoryListener;
-import net.minecraft.server.v1_16_R3.InventoryEnderChest;
-import net.minecraft.server.v1_16_R3.ItemStack;
-import net.minecraft.server.v1_16_R3.NonNullList;
-import org.bukkit.Location;
-import org.bukkit.craftbukkit.v1_16_R3.entity.CraftHumanEntity;
-import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftInventory;
-import org.bukkit.entity.HumanEntity;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryHolder;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public class SpecialEnderChest extends InventoryEnderChest implements ISpecialEnderChest {
-
- private final CraftInventory inventory;
- private EntityPlayer owner;
- private NonNullList items;
- private boolean playerOnline;
-
- public SpecialEnderChest(final Player player, final Boolean online) {
- super(PlayerDataManager.getHandle(player));
- this.inventory = new CraftInventory(this);
- this.owner = PlayerDataManager.getHandle(player);
- this.playerOnline = online;
- this.items = this.owner.getEnderChest().items;
- }
-
- @Override
- public @NotNull CraftInventory getBukkitInventory() {
- return inventory;
- }
-
- @Override
- public boolean isInUse() {
- return !this.getViewers().isEmpty();
- }
-
- @Override
- public void setPlayerOffline() {
- this.playerOnline = false;
- }
-
- @Override
- public void setPlayerOnline(@NotNull final Player player) {
- if (!this.playerOnline) {
- try {
- this.owner = PlayerDataManager.getHandle(player);
- InventoryEnderChest enderChest = owner.getEnderChest();
- for (int i = 0; i < enderChest.getSize(); ++i) {
- enderChest.setItem(i, this.items.get(i));
- }
- this.items = enderChest.items;
- } catch (Exception ignored) {}
- this.playerOnline = true;
- }
- }
-
- @Override
- public void update() {
- this.owner.getEnderChest().update();
- }
-
- @Override
- public List getContents() {
- return this.items;
- }
-
- @Override
- public void onOpen(CraftHumanEntity who) {
- this.owner.getEnderChest().onOpen(who);
- }
-
- @Override
- public void onClose(CraftHumanEntity who) {
- this.owner.getEnderChest().onClose(who);
- }
-
- @Override
- public List getViewers() {
- return this.owner.getEnderChest().getViewers();
- }
-
- @Override
- public void setMaxStackSize(int i) {
- this.owner.getEnderChest().setMaxStackSize(i);
- }
-
- @Override
- public InventoryHolder getOwner() {
- return this.owner.getEnderChest().getOwner();
- }
-
- @Override
- public @Nullable Location getLocation() {
- return null;
- }
-
- @Override
- public void a(IInventoryListener iinventorylistener) {
- this.owner.getEnderChest().a(iinventorylistener);
- }
-
- @Override
- public void b(IInventoryListener iinventorylistener) {
- this.owner.getEnderChest().b(iinventorylistener);
- }
-
- @Override
- public ItemStack getItem(int i) {
- return i >= 0 && i < this.items.size() ? this.items.get(i) : ItemStack.b;
- }
-
- @Override
- public ItemStack splitStack(int i, int j) {
- ItemStack itemstack = ContainerUtil.a(this.items, i, j);
- if (!itemstack.isEmpty()) {
- this.update();
- }
-
- return itemstack;
- }
-
- @Override
- public ItemStack a(ItemStack itemstack) {
- ItemStack itemstack1 = itemstack.cloneItemStack();
-
- for (int i = 0; i < this.getSize(); ++i) {
- ItemStack itemstack2 = this.getItem(i);
- if (itemstack2.isEmpty()) {
- this.setItem(i, itemstack1);
- this.update();
- return ItemStack.b;
- }
-
- if (ItemStack.c(itemstack2, itemstack1)) {
- int j = Math.min(this.getMaxStackSize(), itemstack2.getMaxStackSize());
- int k = Math.min(itemstack1.getCount(), j - itemstack2.getCount());
- if (k > 0) {
- itemstack2.add(k);
- itemstack1.subtract(k);
- if (itemstack1.isEmpty()) {
- this.update();
- return ItemStack.b;
- }
- }
- }
- }
-
- if (itemstack1.getCount() != itemstack.getCount()) {
- this.update();
- }
-
- return itemstack1;
- }
-
- @Override
- public ItemStack splitWithoutUpdate(int i) {
- ItemStack itemstack = this.items.get(i);
- if (itemstack.isEmpty()) {
- return ItemStack.b;
- } else {
- this.items.set(i, ItemStack.b);
- return itemstack;
- }
- }
-
- @Override
- public void setItem(int i, ItemStack itemstack) {
- this.items.set(i, itemstack);
- if (!itemstack.isEmpty() && itemstack.getCount() > this.getMaxStackSize()) {
- itemstack.setCount(this.getMaxStackSize());
- }
-
- this.update();
- }
-
- @Override
- public int getSize() {
- return this.owner.getEnderChest().getSize();
- }
-
- @Override
- public boolean isEmpty() {
-
- for (ItemStack itemstack : this.items) {
- if (!itemstack.isEmpty()) {
- return false;
- }
- }
-
- return true;
- }
-
- @Override
- public int getMaxStackSize() {
- return 64;
- }
-
- @Override
- public boolean a(EntityHuman entityhuman) {
- return true;
- }
-
- @Override
- public void startOpen(EntityHuman entityhuman) {
- }
-
- @Override
- public void closeContainer(EntityHuman entityhuman) {
- }
-
- @Override
- public boolean b(int i, ItemStack itemstack) {
- return true;
- }
-
- @Override
- public void clear() {
- this.items.clear();
- }
-
- @Override
- public void a(AutoRecipeStackManager autorecipestackmanager) {
-
- for (ItemStack itemstack : this.items) {
- autorecipestackmanager.b(itemstack);
- }
-
- }
-
-}
diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java
deleted file mode 100644
index ada345c1..00000000
--- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java
+++ /dev/null
@@ -1,733 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal.v1_16_R3;
-
-import com.google.common.collect.ImmutableList;
-import com.lishid.openinv.internal.ISpecialPlayerInventory;
-import java.util.Iterator;
-import java.util.List;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-import net.minecraft.server.v1_16_R3.AutoRecipeStackManager;
-import net.minecraft.server.v1_16_R3.ChatMessage;
-import net.minecraft.server.v1_16_R3.ContainerUtil;
-import net.minecraft.server.v1_16_R3.CrashReport;
-import net.minecraft.server.v1_16_R3.CrashReportSystemDetails;
-import net.minecraft.server.v1_16_R3.DamageSource;
-import net.minecraft.server.v1_16_R3.EntityHuman;
-import net.minecraft.server.v1_16_R3.EntityPlayer;
-import net.minecraft.server.v1_16_R3.EnumItemSlot;
-import net.minecraft.server.v1_16_R3.IBlockData;
-import net.minecraft.server.v1_16_R3.IChatBaseComponent;
-import net.minecraft.server.v1_16_R3.IInventory;
-import net.minecraft.server.v1_16_R3.Item;
-import net.minecraft.server.v1_16_R3.ItemArmor;
-import net.minecraft.server.v1_16_R3.ItemStack;
-import net.minecraft.server.v1_16_R3.NBTTagCompound;
-import net.minecraft.server.v1_16_R3.NBTTagList;
-import net.minecraft.server.v1_16_R3.NonNullList;
-import net.minecraft.server.v1_16_R3.PacketPlayOutSetSlot;
-import net.minecraft.server.v1_16_R3.PlayerInventory;
-import net.minecraft.server.v1_16_R3.ReportedException;
-import net.minecraft.server.v1_16_R3.World;
-import org.bukkit.Location;
-import org.bukkit.craftbukkit.v1_16_R3.entity.CraftHumanEntity;
-import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftInventory;
-import org.bukkit.entity.HumanEntity;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryHolder;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public class SpecialPlayerInventory extends PlayerInventory implements ISpecialPlayerInventory {
-
- private final CraftInventory inventory;
- private boolean playerOnline;
- private EntityHuman player;
- private NonNullList items, armor, extraSlots;
- private List> f;
-
- public SpecialPlayerInventory(final Player bukkitPlayer, final Boolean online) {
- super(PlayerDataManager.getHandle(bukkitPlayer));
- this.inventory = new CraftInventory(this);
- this.playerOnline = online;
- this.player = super.player;
- this.items = this.player.inventory.items;
- this.armor = this.player.inventory.armor;
- this.extraSlots = this.player.inventory.extraSlots;
- this.f = ImmutableList.of(this.items, this.armor, this.extraSlots);
- }
-
- @Override
- public void setPlayerOnline(@NotNull final Player player) {
- if (!this.playerOnline) {
- EntityPlayer entityPlayer = PlayerDataManager.getHandle(player);
- entityPlayer.inventory.transaction.addAll(this.transaction);
- this.player = entityPlayer;
- for (int i = 0; i < getSize(); ++i) {
- this.player.inventory.setItem(i, getRawItem(i));
- }
- this.player.inventory.itemInHandIndex = this.itemInHandIndex;
- this.items = this.player.inventory.items;
- this.armor = this.player.inventory.armor;
- this.extraSlots = this.player.inventory.extraSlots;
- this.f = ImmutableList.of(this.items, this.armor, this.extraSlots);
- this.playerOnline = true;
- }
- }
-
- @Override
- public boolean a(final EntityHuman entityhuman) {
- return true;
- }
-
- @Override
- public @NotNull CraftInventory getBukkitInventory() {
- return this.inventory;
- }
-
- @Override
- public ItemStack getItem(int i) {
- List list = this.items;
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.armor;
- } else {
- i = this.getReversedItemSlotNum(i);
- }
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.extraSlots;
- } else if (list == this.armor) {
- i = this.getReversedArmorSlotNum(i);
- }
-
- if (i >= list.size()) {
- return ItemStack.b;
- }
-
- return list.get(i);
- }
-
- private ItemStack getRawItem(int i) {
- NonNullList list = null;
- for (NonNullList next : this.f) {
- if (i < next.size()) {
- list = next;
- break;
- }
- i -= next.size();
- }
-
- return list == null ? ItemStack.b : list.get(i);
- }
-
- @Override
- public IChatBaseComponent getDisplayName() {
- return new ChatMessage(this.player.getName());
- }
-
- @Override
- public boolean hasCustomName() {
- return false;
- }
-
- private int getReversedArmorSlotNum(final int i) {
- if (i == 0) {
- return 3;
- }
- if (i == 1) {
- return 2;
- }
- if (i == 2) {
- return 1;
- }
- if (i == 3) {
- return 0;
- }
- return i;
- }
-
- private int getReversedItemSlotNum(final int i) {
- if (i >= 27) {
- return i - 27;
- }
- return i + 9;
- }
-
- @Override
- public int getSize() {
- return 45;
- }
-
- @Override
- public boolean isInUse() {
- return !this.getViewers().isEmpty();
- }
-
- @Override
- public void setItem(int i, final ItemStack itemstack) {
- List list = this.items;
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.armor;
- } else {
- i = this.getReversedItemSlotNum(i);
- }
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.extraSlots;
- } else if (list == this.armor) {
- i = this.getReversedArmorSlotNum(i);
- }
-
- if (i >= list.size()) {
- this.player.drop(itemstack, true);
- return;
- }
-
- list.set(i, itemstack);
- }
-
- @Override
- public void setPlayerOffline() {
- this.playerOnline = false;
- }
-
- @Override
- public ItemStack splitStack(int i, final int j) {
- List list = this.items;
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.armor;
- } else {
- i = this.getReversedItemSlotNum(i);
- }
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.extraSlots;
- } else if (list == this.armor) {
- i = this.getReversedArmorSlotNum(i);
- }
-
- if (i >= list.size()) {
- return ItemStack.b;
- }
-
- return list.get(i).isEmpty() ? ItemStack.b : ContainerUtil.a(list, i, j);
- }
-
- @Override
- public ItemStack splitWithoutUpdate(int i) {
- List list = this.items;
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.armor;
- } else {
- i = this.getReversedItemSlotNum(i);
- }
-
- if (i >= list.size()) {
- i -= list.size();
- list = this.extraSlots;
- } else if (list == this.armor) {
- i = this.getReversedArmorSlotNum(i);
- }
-
- if (i >= list.size()) {
- return ItemStack.b;
- }
-
- if (!list.get(i).isEmpty()) {
- ItemStack itemstack = list.get(i);
-
- list.set(i, ItemStack.b);
- return itemstack;
- }
-
- return ItemStack.b;
- }
-
- @Override
- public List getContents() {
- return this.f.stream().flatMap(List::stream).collect(Collectors.toList());
- }
-
- @Override
- public List getArmorContents() {
- return this.armor;
- }
-
- @Override
- public void onOpen(CraftHumanEntity who) {
- this.transaction.add(who);
- }
-
- @Override
- public void onClose(CraftHumanEntity who) {
- this.transaction.remove(who);
- }
-
- @Override
- public List getViewers() {
- return this.transaction;
- }
-
- @Override
- public InventoryHolder getOwner() {
- return this.player.getBukkitEntity();
- }
-
- @Override
- public Location getLocation() {
- return this.player.getBukkitEntity().getLocation();
- }
-
- @Override
- public ItemStack getItemInHand() {
- return d(this.itemInHandIndex) ? this.items.get(this.itemInHandIndex) : ItemStack.b;
- }
-
- private boolean isSimilarAndNotFull(ItemStack itemstack, ItemStack itemstack1) {
- return !itemstack.isEmpty() && this.b(itemstack, itemstack1) && itemstack.isStackable() && itemstack.getCount() < itemstack.getMaxStackSize() && itemstack.getCount() < this.getMaxStackSize();
- }
-
- private boolean b(ItemStack itemstack, ItemStack itemstack1) {
- return itemstack.getItem() == itemstack1.getItem() && ItemStack.equals(itemstack, itemstack1);
- }
-
- @Override
- public int canHold(ItemStack itemstack) {
- int remains = itemstack.getCount();
-
- for (int i = 0; i < this.items.size(); ++i) {
- ItemStack itemstack1 = this.getItem(i);
- if (itemstack1.isEmpty()) {
- return itemstack.getCount();
- }
-
- if (!this.isSimilarAndNotFull(itemstack, itemstack1)) {
- remains -= Math.min(itemstack1.getMaxStackSize(), this.getMaxStackSize()) - itemstack1.getCount();
- }
-
- if (remains <= 0) {
- return itemstack.getCount();
- }
- }
-
- ItemStack offhandItemStack = this.getItem(this.items.size() + this.armor.size());
- if (this.isSimilarAndNotFull(offhandItemStack, itemstack)) {
- remains -= Math.min(offhandItemStack.getMaxStackSize(), this.getMaxStackSize()) - offhandItemStack.getCount();
- }
-
- return itemstack.getCount() - remains;
- }
-
- @Override
- public int getFirstEmptySlotIndex() {
- for (int i = 0; i < this.items.size(); ++i) {
- if (this.items.get(i).isEmpty()) {
- return i;
- }
- }
-
- return -1;
- }
-
- @Override
- public void c(int i) {
- this.itemInHandIndex = this.i();
- ItemStack itemstack = this.items.get(this.itemInHandIndex);
- this.items.set(this.itemInHandIndex, this.items.get(i));
- this.items.set(i, itemstack);
- }
-
- @Override
- public int c(ItemStack itemstack) {
- for (int i = 0; i < this.items.size(); ++i) {
- ItemStack itemstack1 = this.items.get(i);
- if (!this.items.get(i).isEmpty() && this.b(itemstack, this.items.get(i)) && !this.items.get(i).f() && !itemstack1.hasEnchantments() && !itemstack1.hasName()) {
- return i;
- }
- }
-
- return -1;
- }
-
- @Override
- public int i() {
- int i;
- int j;
- for (j = 0; j < 9; ++j) {
- i = (this.itemInHandIndex + j) % 9;
- if (this.items.get(i).isEmpty()) {
- return i;
- }
- }
-
- for (j = 0; j < 9; ++j) {
- i = (this.itemInHandIndex + j) % 9;
- if (!this.items.get(i).hasEnchantments()) {
- return i;
- }
- }
-
- return this.itemInHandIndex;
- }
-
- @Override
- public int a(Predicate predicate, int i, IInventory iinventory) {
- byte b0 = 0;
- boolean flag = i == 0;
- int j = b0 + ContainerUtil.a(this, predicate, i - b0, flag);
- j += ContainerUtil.a(iinventory, predicate, i - j, flag);
- j += ContainerUtil.a(this.getCarried(), predicate, i - j, flag);
- if (this.getCarried().isEmpty()) {
- this.setCarried(ItemStack.b);
- }
-
- return j;
- }
-
- private int i(ItemStack itemstack) {
- int i = this.firstPartial(itemstack);
- if (i == -1) {
- i = this.getFirstEmptySlotIndex();
- }
-
- return i == -1 ? itemstack.getCount() : this.d(i, itemstack);
- }
-
- private int d(int i, ItemStack itemstack) {
- Item item = itemstack.getItem();
- int j = itemstack.getCount();
- ItemStack itemstack1 = this.getItem(i);
- if (itemstack1.isEmpty()) {
- itemstack1 = new ItemStack(item, 0);
- NBTTagCompound tag = itemstack.getTag();
- if (tag != null) {
- itemstack1.setTag(tag.clone());
- }
-
- this.setItem(i, itemstack1);
- }
-
- int k = j;
- if (j > itemstack1.getMaxStackSize() - itemstack1.getCount()) {
- k = itemstack1.getMaxStackSize() - itemstack1.getCount();
- }
-
- if (k > this.getMaxStackSize() - itemstack1.getCount()) {
- k = this.getMaxStackSize() - itemstack1.getCount();
- }
-
- if (k != 0) {
- j -= k;
- itemstack1.add(k);
- itemstack1.d(5);
- }
- return j;
- }
-
- @Override
- public int firstPartial(ItemStack itemstack) {
- if (this.isSimilarAndNotFull(this.getItem(this.itemInHandIndex), itemstack)) {
- return this.itemInHandIndex;
- } else if (this.isSimilarAndNotFull(this.getItem(40), itemstack)) {
- return 40;
- } else {
- for (int i = 0; i < this.items.size(); ++i) {
- if (this.isSimilarAndNotFull(this.items.get(i), itemstack)) {
- return i;
- }
- }
-
- return -1;
- }
- }
-
- @Override
- public void j() {
-
- for (List itemStacks : this.f) {
- for (int i = 0; i < itemStacks.size(); ++i) {
- if (!itemStacks.get(i).isEmpty()) {
- itemStacks.get(i).a(this.player.world, this.player, i, this.itemInHandIndex == i);
- }
- }
- }
-
- }
-
- @Override
- public boolean pickup(ItemStack itemstack) {
- return this.c(-1, itemstack);
- }
-
- @Override
- public boolean c(int i, ItemStack itemstack) {
- if (itemstack.isEmpty()) {
- return false;
- } else {
- try {
- if (itemstack.f()) {
- if (i == -1) {
- i = this.getFirstEmptySlotIndex();
- }
-
- if (i >= 0) {
- this.items.set(i, itemstack.cloneItemStack());
- this.items.get(i).d(5);
- itemstack.setCount(0);
- return true;
- } else if (this.player.abilities.canInstantlyBuild) {
- itemstack.setCount(0);
- return true;
- } else {
- return false;
- }
- } else {
- int j;
- do {
- j = itemstack.getCount();
- if (i == -1) {
- itemstack.setCount(this.i(itemstack));
- } else {
- itemstack.setCount(this.d(i, itemstack));
- }
- } while(!itemstack.isEmpty() && itemstack.getCount() < j);
-
- if (itemstack.getCount() == j && this.player.abilities.canInstantlyBuild) {
- itemstack.setCount(0);
- return true;
- } else {
- return itemstack.getCount() < j;
- }
- }
- } catch (Throwable var6) {
- CrashReport crashreport = CrashReport.a(var6, "Adding item to inventory");
- CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Item being added");
- crashreportsystemdetails.a("Item ID", Item.getId(itemstack.getItem()));
- crashreportsystemdetails.a("Item data", itemstack.getDamage());
- crashreportsystemdetails.a("Item name", () -> itemstack.getName().getString());
- throw new ReportedException(crashreport);
- }
- }
- }
-
- @Override
- public void a(World world, ItemStack itemstack) {
- if (!world.isClientSide) {
- while(!itemstack.isEmpty()) {
- int i = this.firstPartial(itemstack);
- if (i == -1) {
- i = this.getFirstEmptySlotIndex();
- }
-
- if (i == -1) {
- this.player.drop(itemstack, false);
- break;
- }
-
- int j = itemstack.getMaxStackSize() - this.getItem(i).getCount();
- if (this.c(i, itemstack.cloneAndSubtract(j))) {
- ((EntityPlayer)this.player).playerConnection.sendPacket(new PacketPlayOutSetSlot(-2, i, this.getItem(i)));
- }
- }
- }
-
- }
-
- @Override
- public void f(ItemStack itemstack) {
-
- for (List list : this.f) {
- for (int i = 0; i < list.size(); ++i) {
- if (list.get(i) == itemstack) {
- list.set(i, ItemStack.b);
- break;
- }
- }
- }
- }
-
- @Override
- public float a(IBlockData iblockdata) {
- return this.items.get(this.itemInHandIndex).a(iblockdata);
- }
-
- @Override
- public NBTTagList a(NBTTagList nbttaglist) {
- NBTTagCompound nbttagcompound;
- int i;
- for (i = 0; i < this.items.size(); ++i) {
- if (!this.items.get(i).isEmpty()) {
- nbttagcompound = new NBTTagCompound();
- nbttagcompound.setByte("Slot", (byte) i);
- this.items.get(i).save(nbttagcompound);
- nbttaglist.add(nbttagcompound);
- }
- }
-
- for (i = 0; i < this.armor.size(); ++i) {
- if (!this.armor.get(i).isEmpty()) {
- nbttagcompound = new NBTTagCompound();
- nbttagcompound.setByte("Slot", (byte) (i + 100));
- this.armor.get(i).save(nbttagcompound);
- nbttaglist.add(nbttagcompound);
- }
- }
-
- for (i = 0; i < this.extraSlots.size(); ++i) {
- if (!this.extraSlots.get(i).isEmpty()) {
- nbttagcompound = new NBTTagCompound();
- nbttagcompound.setByte("Slot", (byte) (i + 150));
- this.extraSlots.get(i).save(nbttagcompound);
- nbttaglist.add(nbttagcompound);
- }
- }
-
- return nbttaglist;
- }
-
- @Override
- public void b(NBTTagList nbttaglist) {
- this.items.clear();
- this.armor.clear();
- this.extraSlots.clear();
-
- for(int i = 0; i < nbttaglist.size(); ++i) {
- NBTTagCompound nbttagcompound = nbttaglist.getCompound(i);
- int j = nbttagcompound.getByte("Slot") & 255;
- ItemStack itemstack = ItemStack.a(nbttagcompound);
- if (!itemstack.isEmpty()) {
- if (j < this.items.size()) {
- this.items.set(j, itemstack);
- } else if (j >= 100 && j < this.armor.size() + 100) {
- this.armor.set(j - 100, itemstack);
- } else if (j >= 150 && j < this.extraSlots.size() + 150) {
- this.extraSlots.set(j - 150, itemstack);
- }
- }
- }
-
- }
-
- @Override
- public boolean isEmpty() {
- Iterator iterator = this.items.iterator();
-
- ItemStack itemstack;
- while (iterator.hasNext()) {
- itemstack = iterator.next();
- if (!itemstack.isEmpty()) {
- return false;
- }
- }
-
- iterator = this.armor.iterator();
-
- while (iterator.hasNext()) {
- itemstack = iterator.next();
- if (!itemstack.isEmpty()) {
- return false;
- }
- }
-
- iterator = this.extraSlots.iterator();
-
- while (iterator.hasNext()) {
- itemstack = iterator.next();
- if (!itemstack.isEmpty()) {
- return false;
- }
- }
-
- return true;
- }
-
- @Nullable
- @Override
- public IChatBaseComponent getCustomName() {
- return null;
- }
-
- @Override
- public void a(DamageSource damagesource, float f) {
- if (f > 0.0F) {
- f /= 4.0F;
- if (f < 1.0F) {
- f = 1.0F;
- }
-
- for (int i = 0; i < this.armor.size(); ++i) {
- ItemStack itemstack = this.armor.get(0);
- int index = i;
- if ((!damagesource.isFire() || !itemstack.getItem().u()) && itemstack.getItem() instanceof ItemArmor) {
- itemstack.damage((int) f, this.player, (entityHuman) -> entityHuman.broadcastItemBreak(EnumItemSlot.a(EnumItemSlot.Function.ARMOR, index)));
- }
- }
- }
- }
-
- @Override
- public void dropContents() {
- for (List itemStacks : this.f) {
- for (int i = 0; i < itemStacks.size(); ++i) {
- ItemStack itemstack = itemStacks.get(i);
- if (!itemstack.isEmpty()) {
- itemStacks.set(i, ItemStack.b);
- this.player.a(itemstack, true, false);
- }
- }
- }
- }
-
- @Override
- public boolean h(ItemStack itemstack) {
- return this.f.stream().flatMap(List::stream).anyMatch(itemStack1 -> !itemStack1.isEmpty() && itemStack1.doMaterialsMatch(itemstack));
- }
-
- @Override
- public void a(PlayerInventory playerinventory) {
- for (int i = 0; i < playerinventory.getSize(); ++i) {
- this.setItem(i, playerinventory.getItem(i));
- }
-
- this.itemInHandIndex = playerinventory.itemInHandIndex;
- }
-
- @Override
- public void clear() {
- this.f.forEach(List::clear);
- }
-
- @Override
- public void a(AutoRecipeStackManager autorecipestackmanager) {
- for (ItemStack itemstack : this.items) {
- autorecipestackmanager.a(itemstack);
- }
- }
-
-}
diff --git a/jitpack.yml b/jitpack.yml
new file mode 100644
index 00000000..4130c60e
--- /dev/null
+++ b/jitpack.yml
@@ -0,0 +1,6 @@
+before_install:
+ - sdk update
+ - sdk install java 21-tem
+ - sdk use java 21-tem
+install:
+ - ./gradlew -Djitpack=true :openinvapi:publishJitpackPublicationToMavenLocal
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
new file mode 100644
index 00000000..d00a60a6
--- /dev/null
+++ b/plugin/build.gradle.kts
@@ -0,0 +1,53 @@
+import com.github.jikoo.openinv.SpigotReobf
+
+plugins {
+ `openinv-base`
+ alias(libs.plugins.shadow)
+}
+
+repositories {
+ maven("https://jitpack.io")
+}
+
+dependencies {
+ implementation(project(":openinvapi"))
+ implementation(project(":openinvcommon"))
+ implementation(project(":openinvadaptercommon"))
+ implementation(project(":openinvadapterpaper1_21_10"))
+ implementation(project(":openinvadapterpaper1_21_8"))
+ implementation(project(":openinvadapterpaper1_21_5"))
+ implementation(project(":openinvadapterpaper1_21_4"))
+ implementation(project(":openinvadapterpaper1_21_3"))
+ implementation(project(":openinvadapterpaper1_21_1"))
+ implementation(project(":openinvadapterspigot", configuration = SpigotReobf.ARTIFACT_CONFIG))
+ implementation(libs.planarwrappers)
+ implementation(libs.folia.scheduler.wrapper)
+}
+
+tasks.processResources {
+ expand("version" to version)
+}
+
+tasks.jar {
+ manifest.attributes("paperweight-mappings-namespace" to "mojang")
+}
+
+tasks.shadowJar {
+ relocate("me.nahu.scheduler.wrapper", "com.github.jikoo.openinv.lib.nahu.scheduler-wrapper")
+ relocate("com.github.jikoo.planarwrappers", "com.github.jikoo.openinv.lib.planarwrappers")
+ minimize {
+ exclude(":openinv**")
+ exclude(dependency(libs.folia.scheduler.wrapper.get()))
+ }
+}
+
+tasks.register("distributePlugin") {
+ into(rootProject.layout.projectDirectory.dir("dist"))
+ from(tasks.shadowJar)
+ rename("openinvplugin.*\\.jar", "OpenInv.jar")
+}
+
+tasks.assemble {
+ dependsOn(tasks.shadowJar)
+ dependsOn(tasks.named("distributePlugin"))
+}
diff --git a/plugin/pom.xml b/plugin/pom.xml
deleted file mode 100644
index 358b3962..00000000
--- a/plugin/pom.xml
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
- 4.0.0
-
-
- com.lishid
- openinvparent
- 4.1.6-SNAPSHOT
-
-
- openinvplugincore
- OpenInvPlugin
-
-
-
- com.lishid
- openinvapi
- 4.1.6-SNAPSHOT
-
-
- org.spigotmc
- spigot-api
- 1.15.2-R0.1-SNAPSHOT
- provided
-
-
-
-
-
-
- src/main/resources
- true
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.2.2
-
- true
-
-
-
- package
-
- shade
-
-
-
-
-
-
- maven-compiler-plugin
- 3.8.1
-
- 1.8
- 1.8
-
-
-
-
-
-
diff --git a/plugin/src/main/java/com/lishid/openinv/OpenInv.java b/plugin/src/main/java/com/lishid/openinv/OpenInv.java
index 81375843..e56d9d22 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-2020 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,35 +16,30 @@
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;
-import com.lishid.openinv.commands.SearchEnchantCommand;
-import com.lishid.openinv.commands.SearchInvCommand;
+import com.lishid.openinv.command.ClearInvCommand;
+import com.lishid.openinv.command.ContainerSettingCommand;
+import com.lishid.openinv.command.OpenInvCommand;
+import com.lishid.openinv.command.SearchContainerCommand;
+import com.lishid.openinv.command.SearchEnchantCommand;
+import com.lishid.openinv.command.SearchInvCommand;
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.listener.ContainerListener;
+import com.lishid.openinv.listener.ToggleListener;
+import com.lishid.openinv.util.AccessEqualMode;
import com.lishid.openinv.util.InternalAccessor;
-import com.lishid.openinv.util.LanguageManager;
+import com.lishid.openinv.util.InventoryManager;
import com.lishid.openinv.util.Permissions;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-import net.md_5.bungee.api.ChatMessageType;
-import net.md_5.bungee.api.chat.TextComponent;
-import org.bukkit.Bukkit;
+import com.lishid.openinv.util.PlayerLoader;
+import com.lishid.openinv.util.config.Config;
+import com.lishid.openinv.util.config.ConfigUpdater;
+import com.lishid.openinv.util.lang.LangMigrator;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.setting.PlayerToggle;
+import com.lishid.openinv.util.setting.PlayerToggles;
+import me.nahu.scheduler.wrapper.FoliaWrappedJavaPlugin;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@@ -52,516 +47,272 @@
import org.bukkit.command.PluginCommand;
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;
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.function.Consumer;
+
/**
- * Open other player's inventory
- *
- * @author lishid
+ * The main class for OpenInv.
*/
-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 = 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 = inv.getViewers();
- for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) {
- entity.closeInventory();
- }
- }
-
- if (!OpenInv.this.disableSaving() && !value.isOnline()) {
- value.saveData();
- }
- });
-
- private InternalAccessor accessor;
- private LanguageManager languageManager;
-
- /**
- * Evicts 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;
- }
-
- if (this.inventories.containsKey(key)) {
- Iterator iterator = this.inventories.get(key).getBukkitInventory().getViewers().iterator();
- //noinspection WhileLoopReplaceableByForEach
- while (iterator.hasNext()) {
- HumanEntity human = iterator.next();
- // 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) || human.getWorld().equals(player.getWorld())) {
- continue;
- }
- human.closeInventory();
- }
- }
-
- if (this.enderChests.containsKey(key)) {
- Iterator iterator = this.enderChests.get(key).getBukkitInventory().getViewers().iterator();
- //noinspection WhileLoopReplaceableByForEach
- while (iterator.hasNext()) {
- HumanEntity human = iterator.next();
- if (Permissions.CROSSWORLD.hasPermission(human) || human.getWorld().equals(player.getWorld())) {
- continue;
- }
- human.closeInventory();
- }
- }
- }
-
- /**
- * Convert a raw slot number into a player inventory slot number.
- *
- *
Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular
- * player inventory slot number.
- *
- * @param view the open inventory view
- * @param rawSlot the raw slot in the view
- * @return the converted slot number
- */
- public int convertToPlayerSlot(InventoryView view, int rawSlot) {
- return this.accessor.getPlayerDataManager().convertToPlayerSlot(view, rawSlot);
- }
-
- @Override
- public boolean disableSaving() {
- return this.getConfig().getBoolean("settings.disable-saving", false);
- }
-
- @NotNull
- @Override
- public IAnySilentContainer getAnySilentContainer() {
- return this.accessor.getAnySilentContainer();
- }
-
- @Override
- public boolean getPlayerAnyChestStatus(@NotNull final OfflinePlayer player) {
- boolean defaultState = false;
-
- if (player.isOnline()) {
- Player onlinePlayer = player.getPlayer();
- if (onlinePlayer != null) {
- defaultState = Permissions.ANY_DEFAULT.hasPermission(onlinePlayer);
- }
- }
-
- return this.getConfig().getBoolean("toggles.any-chest." + this.getPlayerID(player), defaultState);
- }
-
- @Override
- public boolean getPlayerSilentChestStatus(@NotNull final OfflinePlayer offline) {
- boolean defaultState = false;
-
- if (offline.isOnline()) {
- Player onlinePlayer = offline.getPlayer();
- if (onlinePlayer != null) {
- defaultState = Permissions.SILENT_DEFAULT.hasPermission(onlinePlayer);
- }
- }
-
- return this.getConfig().getBoolean("toggles.silent-chest." + this.getPlayerID(offline), defaultState);
- }
-
- @NotNull
- @Override
- public 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);
- }
- ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online);
- this.enderChests.put(id, inv);
- this.playerCache.put(id, player);
- return inv;
- }
-
- @NotNull
- @Override
- public 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);
- }
- ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online);
- this.inventories.put(id, inv);
- this.playerCache.put(id, player);
- return inv;
- }
-
- @Override
- public boolean isSupportedVersion() {
- return this.accessor != null && this.accessor.isSupported();
- }
-
- @Override
- public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
-
- String key = this.getPlayerID(offline);
- if (this.playerCache.containsKey(key)) {
- return this.playerCache.get(key);
- }
-
- Player player = offline.getPlayer();
- if (player != null) {
- this.playerCache.put(key, player);
- return player;
- }
-
- if (!this.isSupportedVersion()) {
- return null;
- }
-
- if (Bukkit.isPrimaryThread()) {
- return this.accessor.getPlayerDataManager().loadPlayer(offline);
- }
-
- Future future = Bukkit.getScheduler().callSyncMethod(this,
- () -> OpenInv.this.accessor.getPlayerDataManager().loadPlayer(offline));
-
- try {
- player = future.get();
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- return null;
- }
-
- if (player != null) {
- this.playerCache.put(key, player);
- }
-
- return player;
- }
-
- @Override
- public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
- return this.accessor.getPlayerDataManager().openInventory(player, inventory);
- }
-
- public void sendMessage(@NotNull CommandSender sender, @NotNull String key) {
- String message = this.languageManager.getValue(key, getLocale(sender));
-
- if (message != null && !message.isEmpty()) {
- sender.sendMessage(message);
- }
- }
-
- public void sendMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) {
- String message = this.languageManager.getValue(key, getLocale(sender), replacements);
-
- if (message != null && !message.isEmpty()) {
- sender.sendMessage(message);
- }
- }
-
- public void sendSystemMessage(@NotNull Player player, @NotNull String key) {
- String message = this.languageManager.getValue(key, getLocale(player));
-
- if (message == null) {
- return;
- }
-
- int newline = message.indexOf('\n');
- if (newline != -1) {
- // No newlines in action bar chat.
- message = message.substring(0, newline);
- }
-
- if (message.isEmpty()) {
- return;
- }
-
- player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message));
- }
-
- public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key) {
- return this.languageManager.getValue(key, getLocale(sender));
- }
-
- public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) {
- return this.languageManager.getValue(key, getLocale(sender), replacements);
- }
-
- private @Nullable String getLocale(@NotNull CommandSender sender) {
- if (sender instanceof Player) {
- return ((Player) sender).getLocale();
- } else {
- return this.getConfig().getString("settings.locale", "en_us");
- }
- }
-
- @Override
- public boolean notifyAnyChest() {
- return this.getConfig().getBoolean("notify.any-chest", true);
- }
-
- @Override
- public boolean notifySilentChest() {
- return this.getConfig().getBoolean("notify.silent-chest", true);
- }
-
- @Override
- public void onDisable() {
-
- if (this.disableSaving()) {
- return;
- }
-
- if (this.isSupportedVersion()) {
- this.playerCache.invalidateAll();
- }
- }
-
- @Override
- public void onEnable() {
-
- // Save default configuration if not present.
- this.saveDefaultConfig();
-
- // Get plugin manager
- PluginManager pm = this.getServer().getPluginManager();
-
- this.accessor = new InternalAccessor(this);
-
- this.languageManager = new LanguageManager(this, "en_us");
-
- // Version check
- if (this.accessor.isSupported()) {
- // Update existing configuration. May require internal access.
- new ConfigUpdater(this).checkForUpdates();
-
- // 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
- this.setCommandExecutor(new OpenInvCommand(this), "openinv", "openender");
- this.setCommandExecutor(new SearchContainerCommand(this), "searchcontainer");
- this.setCommandExecutor(new SearchInvCommand(this), "searchinv", "searchender");
- this.setCommandExecutor(new SearchEnchantCommand(this), "searchenchant");
- this.setCommandExecutor(new ContainerSettingCommand(this), "silentcontainer", "anycontainer");
-
- } else {
- this.sendVersionError(this.getLogger()::warning);
- }
-
- }
-
- private void sendVersionError(Consumer messageMethod) {
- messageMethod.accept("Your server version (" + this.accessor.getVersion() + ") is not supported.");
- messageMethod.accept("Please obtain an appropriate version here: " + accessor.getReleasesLink());
- }
-
- private void setCommandExecutor(CommandExecutor executor, String... commands) {
- for (String commandName : commands) {
- PluginCommand command = this.getCommand(commandName);
- if (command != null) {
- command.setExecutor(executor);
- }
- }
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!this.accessor.isSupported()) {
- this.sendVersionError(sender::sendMessage);
- return true;
- }
- 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());
- }
-
- @Override
- public void setPlayerAnyChestStatus(@NotNull final OfflinePlayer offline, final boolean status) {
- this.getConfig().set("toggles.any-chest." + this.getPlayerID(offline), status);
- this.saveConfig();
- }
-
- /**
- * 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) {
-
- 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;
- }
-
- // 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 (this.enderChests.containsKey(key)) {
- this.enderChests.get(key).setPlayerOffline();
- }
- }
-
- /**
- * Method for handling a Player coming online.
- *
- * @param player the Player
- * @throws IllegalStateException if the server version is unsupported
- */
- public void setPlayerOnline(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;
- }
-
- this.playerCache.put(key, 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);
- }
-
- if (this.enderChests.containsKey(key)) {
- this.enderChests.get(key).setPlayerOnline(player);
- }
- }
-
- @Override
- public void setPlayerSilentChestStatus(@NotNull final OfflinePlayer offline, final boolean status) {
- this.getConfig().set("toggles.silent-chest." + this.getPlayerID(offline), status);
- this.saveConfig();
- }
-
- /**
- * Displays all applicable help for OpenInv commands.
- *
- * @param player the Player to help
- */
- public void showHelp(final Player player) {
- // Get registered commands
- for (String commandName : this.getDescription().getCommands().keySet()) {
- PluginCommand command = this.getCommand(commandName);
-
- // Ensure command is successfully registered and player can use it
- if (command == null || !command.testPermissionSilent(player)) {
- continue;
- }
-
- // Send usage
- player.sendMessage(command.getUsage().replace("", commandName));
-
- List aliases = command.getAliases();
- if (aliases.isEmpty()) {
- continue;
- }
-
- // Assemble alias list
- StringBuilder aliasBuilder = new StringBuilder(" (aliases: ");
- for (String alias : aliases) {
- aliasBuilder.append(alias).append(", ");
- }
- aliasBuilder.delete(aliasBuilder.length() - 2, aliasBuilder.length()).append(')');
-
- // Send all aliases
- player.sendMessage(aliasBuilder.toString());
- }
- }
-
- @Override
- public void unload(@NotNull final OfflinePlayer offline) {
- this.playerCache.invalidate(this.getPlayerID(offline));
- }
+public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
+
+ private InternalAccessor accessor;
+ private Config config;
+ private InventoryManager inventoryManager;
+ private LanguageManager languageManager;
+ private PlayerLoader playerLoader;
+ private boolean isSpigot = false;
+
+ @Override
+ public void reloadConfig() {
+ super.reloadConfig();
+ config.reload(getConfig());
+ languageManager.reload();
+ if (accessor != null && accessor.isSupported()) {
+ accessor.reload(getConfig());
+ }
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!isSpigot || !this.accessor.isSupported()) {
+ this.sendVersionError(sender::sendMessage);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDisable() {
+ inventoryManager.evictAll();
+ }
+
+ @Override
+ public void onEnable() {
+ // Save default configuration if not present.
+ this.saveDefaultConfig();
+
+ // Migrate locale files to a subfolder.
+ Path dataFolder = getDataFolder().toPath();
+ new LangMigrator(dataFolder, dataFolder.resolve("locale"), getLogger()).migrate();
+
+ // Set up configurable features. Note that #reloadConfig is called on the first call to #getConfig!
+ // Configuration values should not be accessed until after all of these have been set up.
+ config = new Config();
+ languageManager = new LanguageManager(this, "en");
+ accessor = new InternalAccessor(getLogger(), languageManager);
+
+ // Perform initial config load.
+ reloadConfig();
+
+ inventoryManager = new InventoryManager(this, config, accessor);
+ playerLoader = new PlayerLoader(this, config, inventoryManager, accessor, getLogger());
+
+ try {
+ Class.forName("org.bukkit.entity.Player$Spigot");
+ isSpigot = true;
+ } catch (ClassNotFoundException e) {
+ isSpigot = false;
+ }
+
+ // Version check
+ if (isSpigot && this.accessor.isSupported()) {
+ reloadConfig();
+
+ // Update existing configuration. May require internal access.
+ new ConfigUpdater(this).checkForUpdates();
+
+ // Register relevant event listeners.
+ registerEvents();
+
+ // Register commands to their executors.
+ registerCommands();
+
+ } else {
+ this.sendVersionError(this.getLogger()::warning);
+ }
+
+ }
+
+ private void registerEvents() {
+ PluginManager pluginManager = this.getServer().getPluginManager();
+ pluginManager.registerEvents(playerLoader, this);
+ pluginManager.registerEvents(inventoryManager, this);
+ pluginManager.registerEvents(new ContainerListener(accessor, languageManager), this);
+ pluginManager.registerEvents(new ToggleListener(), this);
+ }
+
+ private void registerCommands() {
+ this.setCommandExecutor(new OpenInvCommand(this, config, inventoryManager, languageManager, playerLoader), "openinv", "openender");
+ this.setCommandExecutor(new SearchContainerCommand(this, languageManager), "searchcontainer");
+ this.setCommandExecutor(new SearchInvCommand(languageManager), "searchinv", "searchender");
+ this.setCommandExecutor(new SearchEnchantCommand(languageManager), "searchenchant");
+ this.setCommandExecutor(new ClearInvCommand(this, config, inventoryManager, languageManager, playerLoader), "clearinv", "clearender");
+
+ ContainerSettingCommand settingCommand = new ContainerSettingCommand(languageManager);
+ for (PlayerToggle toggle : PlayerToggles.get()) {
+ setCommandExecutor(settingCommand, toggle.getName().toLowerCase(Locale.ENGLISH));
+ }
+ }
+
+ private void setCommandExecutor(@NotNull CommandExecutor executor, String @NotNull ... commands) {
+ for (String commandName : commands) {
+ PluginCommand command = this.getCommand(commandName);
+ if (command != null) {
+ command.setExecutor(executor);
+ }
+ }
+ }
+
+ private void sendVersionError(@NotNull Consumer messageMethod) {
+ if (!accessor.isSupported()) {
+ messageMethod.accept("Your server version (" + accessor.getVersion() + ") is not supported.");
+ messageMethod.accept("Please download the correct version of OpenInv here: " + accessor.getReleasesLink());
+ }
+ if (!isSpigot) {
+ messageMethod.accept("OpenInv requires that you use Spigot or a Spigot fork. Per the 1.14 update thread");
+ messageMethod.accept("(https://www.spigotmc.org/threads/369724/ \"A Note on CraftBukkit\"), if you are");
+ messageMethod.accept("encountering an inconsistency with vanilla that prevents you from using Spigot,");
+ messageMethod.accept("that is considered a Spigot bug and should be reported as such.");
+ }
+ }
+
+ @Override
+ public boolean isSupportedVersion() {
+ return this.accessor != null && this.accessor.isSupported();
+ }
+
+ @Override
+ public boolean disableSaving() {
+ return config.isSaveDisabled();
+ }
+
+ @Override
+ public boolean disableOfflineAccess() {
+ return config.isOfflineDisabled();
+ }
+
+ @Override
+ public boolean noArgsOpensSelf() {
+ return config.doesNoArgsOpenSelf();
+ }
+
+ @Override
+ public @NotNull IAnySilentContainer getAnySilentContainer() {
+ return this.accessor.getAnySilentContainer();
+ }
+
+ @Override
+ public boolean getAnyContainerStatus(@NotNull final OfflinePlayer offline) {
+ return PlayerToggles.any().is(offline.getUniqueId());
+ }
+
+ @Override
+ public void setAnyContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
+ PlayerToggles.any().set(offline.getUniqueId(), status);
+ }
+
+ @Override
+ public boolean getSilentContainerStatus(@NotNull final OfflinePlayer offline) {
+ return PlayerToggles.silent().is(offline.getUniqueId());
+ }
+
+ @Override
+ public void setSilentContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
+ PlayerToggles.silent().set(offline.getUniqueId(), status);
+ }
+
+ @Override
+ public @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) {
+ return inventoryManager.getEnderChest(player);
+ }
+
+ @Override
+ public @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) {
+ return inventoryManager.getInventory(player);
+ }
+
+ @Override
+ @Deprecated(forRemoval = true)
+ public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
+ Permissions edit = null;
+ HumanEntity target = inventory.getPlayer();
+ boolean ownContainer = player.equals(target);
+ if (inventory instanceof ISpecialPlayerInventory) {
+ edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER;
+ } else if (inventory instanceof ISpecialEnderChest) {
+ edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_EDIT_OTHER;
+ }
+
+ boolean viewOnly = edit != null && !edit.hasPermission(player);
+
+ if (ownContainer) {
+ return this.accessor.openInventory(player, inventory, viewOnly);
+ }
+
+ AccessEqualMode accessMode = AccessEqualMode.getByPerm(player, config);
+
+ for (int level = 4; level > 0; --level) {
+ String permission = "openinv.access.level." + level;
+ // If the target doesn't have this access level...
+ if (!target.hasPermission(permission)) {
+ // If the viewer does have the access level, all good.
+ if (player.hasPermission(permission)) {
+ break;
+ }
+ // Otherwise check next access level.
+ continue;
+ }
+
+ // If the viewer doesn't have an equal access level or equal access is a denial, deny.
+ if (!player.hasPermission(permission) || accessMode == AccessEqualMode.DENY) {
+ return null;
+ }
+
+ // Since this is a tie, setting decides view state.
+ if (accessMode == AccessEqualMode.VIEW) {
+ viewOnly = true;
+ }
+ break;
+ }
+
+ return openInventory(player, inventory, viewOnly);
+ }
+
+ @Override
+ public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly) {
+ return this.accessor.openInventory(player, inventory, viewOnly);
+ }
+
+ @Override
+ public boolean isPlayerLoaded(@NotNull UUID playerUuid) {
+ return inventoryManager.getLoadedPlayer(playerUuid) != null;
+ }
+
+ @Override
+ public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
+ return playerLoader.load(offline);
+ }
+
+ @Override
+ public @Nullable OfflinePlayer matchPlayer(@NotNull String name) {
+ return playerLoader.match(name);
+ }
+
+ @Override
+ public void unload(@NotNull final OfflinePlayer offline) {
+ inventoryManager.unload(offline.getUniqueId());
+ }
}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java
new file mode 100644
index 00000000..1aa2da15
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java
@@ -0,0 +1,129 @@
+package com.lishid.openinv.command;
+
+import com.lishid.openinv.OpenInv;
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.util.InventoryManager;
+import com.lishid.openinv.util.Permissions;
+import com.lishid.openinv.util.PlayerLoader;
+import com.lishid.openinv.util.config.Config;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.logging.Level;
+
+public class ClearInvCommand extends PlayerLookupCommand {
+
+ private final @NotNull InventoryManager manager;
+
+ public ClearInvCommand(
+ @NotNull OpenInv plugin,
+ @NotNull Config config,
+ @NotNull InventoryManager manager,
+ @NotNull LanguageManager lang,
+ @NotNull PlayerLoader playerLoader
+ ) {
+ super(plugin, lang, config, playerLoader);
+ this.manager = manager;
+ }
+
+ @Override
+ protected boolean isAccessInventory(@NotNull Command command) {
+ return command.getName().equals("clearinv");
+ }
+
+ @Override
+ protected @Nullable String getTargetIdentifer(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @Nullable String argument,
+ boolean accessInv
+ ) {
+ if (!Permissions.CLEAR_OTHER.hasPermission(sender)) {
+ // If the sender does not have permissions to clear others, use self.
+ if (sender instanceof Player player) {
+ return player.getUniqueId().toString();
+ }
+
+ // Console can't target itself. Send error.
+ lang.sendMessage(sender, "messages.error.permissionOpenOther");
+ return null;
+ }
+
+ // If argument is provided, use it.
+ if (argument != null) {
+ return argument;
+ }
+
+ // For players, default to self.
+ if (sender instanceof Player player) {
+ return player.getUniqueId().toString();
+ }
+
+ // For console, argument is required. Send usage.
+ sender.sendMessage(command.getUsage());
+ return null;
+ }
+
+ @Override
+ protected @Nullable OfflinePlayer getTarget(@NotNull String identifier) {
+ return playerLoader.matchExact(identifier);
+ }
+
+ @Override
+ protected boolean deniedCommand(@NotNull CommandSender sender, @NotNull Player onlineTarget, boolean accessInv) {
+ if (onlineTarget.equals(sender)) {
+ return !Permissions.CLEAR_SELF.hasPermission(sender);
+ }
+ return !Permissions.CLEAR_OTHER.hasPermission(sender);
+ }
+
+ @Override
+ protected void handle(
+ @NotNull CommandSender sender,
+ @NotNull PlayerAccess playerAccess,
+ boolean accessInv,
+ @NotNull String @NotNull [] args
+ ) {
+ Player onlineTarget = playerAccess.player();
+ // Create the inventory
+ final ISpecialInventory inv;
+ try {
+ inv = accessInv ? manager.getInventory(onlineTarget) : manager.getEnderChest(onlineTarget);
+ } catch (Exception e) {
+ lang.sendMessage(sender, "messages.error.commandException");
+ plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e);
+ return;
+ }
+
+ // Clear the inventory
+ inv.getBukkitInventory().clear();
+ manager.save(onlineTarget.getUniqueId());
+ lang.sendMessage(
+ sender,
+ accessInv ? "messages.info.clear.inventory" : "messages.info.clear.enderchest",
+ new Replacement("%target%", onlineTarget.getDisplayName())
+ );
+ }
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!Permissions.CLEAR_OTHER.hasPermission(sender)) {
+ return List.of();
+ }
+
+ return super.onTabComplete(sender, command, label, args);
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java b/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java
new file mode 100644
index 00000000..e7b1c6b8
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java
@@ -0,0 +1,118 @@
+/*
+ * 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.command;
+
+import com.lishid.openinv.event.OpenEvents;
+import com.lishid.openinv.util.TabCompleter;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import com.lishid.openinv.util.setting.PlayerToggle;
+import com.lishid.openinv.util.setting.PlayerToggles;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+public class ContainerSettingCommand implements TabExecutor {
+
+ private final @NotNull LanguageManager lang;
+
+ public ContainerSettingCommand(@NotNull LanguageManager lang) {
+ this.lang = lang;
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!(sender instanceof Player player)) {
+ lang.sendMessage(sender, "messages.error.consoleUnsupported");
+ return true;
+ }
+
+ PlayerToggle toggle = PlayerToggles.get(command.getName());
+
+ // Shouldn't be possible.
+ if (toggle == null) {
+ JavaPlugin.getProvidingPlugin(getClass()).getLogger().warning("Command /" + command.getName() + " registered with no corresponding toggle!");
+ return false;
+ }
+
+ UUID playerId = player.getUniqueId();
+
+ if (args.length > 0) {
+ args[0] = args[0].toLowerCase(Locale.ENGLISH);
+
+ if (args[0].equals("on")) {
+ set(toggle, playerId, true);
+ } else if (args[0].equals("off")) {
+ set(toggle, playerId, false);
+ } else if (!args[0].equals("check")) {
+ // Invalid argument, show usage.
+ return false;
+ }
+
+ } else {
+ set(toggle, playerId, !toggle.is(playerId));
+ }
+
+ String onOff = lang.getLocalizedMessage(player, toggle.is(playerId) ? "messages.info.on" : "messages.info.off");
+ if (onOff == null) {
+ onOff = String.valueOf(toggle.is(playerId));
+ }
+
+ lang.sendMessage(
+ sender,
+ "messages.info.settingState",
+ new Replacement("%setting%", toggle.getName()),
+ new Replacement("%state%", onOff)
+ );
+
+ return true;
+ }
+
+ private void set(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean state) {
+ if (toggle.set(uuid, state)) {
+ OpenEvents.notifyPlayerToggle(toggle, uuid, state);
+ }
+ }
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!command.testPermissionSilent(sender) || args.length != 1) {
+ return Collections.emptyList();
+ }
+
+ return TabCompleter.completeString(args[0], new String[]{"check", "on", "off"});
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java
new file mode 100644
index 00000000..14abf66b
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java
@@ -0,0 +1,179 @@
+/*
+ * 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.command;
+
+import com.lishid.openinv.OpenInv;
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.util.InventoryManager;
+import com.lishid.openinv.util.Permissions;
+import com.lishid.openinv.util.PlayerLoader;
+import com.lishid.openinv.util.config.Config;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.PluginCommand;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+
+public class OpenInvCommand extends PlayerLookupCommand {
+
+ private final @NotNull InventoryManager manager;
+ private final Map openInvHistory = new WeakHashMap<>();
+ private final Map openEnderHistory = new WeakHashMap<>();
+
+ public OpenInvCommand(
+ @NotNull OpenInv plugin,
+ @NotNull Config config,
+ @NotNull InventoryManager manager,
+ @NotNull LanguageManager lang,
+ @NotNull PlayerLoader playerLoader
+ ) {
+ super(plugin, lang, config, playerLoader);
+ this.manager = manager;
+ }
+
+ @Override
+ protected boolean isAccessInventory(@NotNull Command command) {
+ return command.getName().equals("openinv");
+ }
+
+ @Override
+ protected @Nullable String getTargetIdentifer(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @Nullable String argument,
+ boolean accessInv
+ ) {
+ // /openinv help
+ if (accessInv && argument != null && (argument.equalsIgnoreCase("help") || argument.equals("?"))) {
+ this.showHelp(sender);
+ return null;
+ }
+
+ // Command is player-only.
+ if (!(sender instanceof Player player)) {
+ lang.sendMessage(sender, "messages.error.consoleUnsupported");
+ return null;
+ }
+
+ // Use fallthrough for no name provided.
+ if (argument == null) {
+ if (config.doesNoArgsOpenSelf()) {
+ return player.getUniqueId().toString();
+ }
+ return (accessInv ? this.openInvHistory : this.openEnderHistory)
+ .computeIfAbsent(player, localPlayer -> localPlayer.getUniqueId().toString());
+ }
+
+ if (!config.doesNoArgsOpenSelf()) {
+ // History management
+ (accessInv ? this.openInvHistory : this.openEnderHistory).put(player, argument);
+ }
+
+ return argument;
+ }
+
+ private void showHelp(@NotNull CommandSender sender) {
+ // Get registered commands
+ for (String commandName : plugin.getDescription().getCommands().keySet()) {
+ PluginCommand command = plugin.getCommand(commandName);
+
+ // Ensure command is successfully registered and sender can use it
+ if (command == null || !command.testPermissionSilent(sender)) {
+ continue;
+ }
+
+ // Send usage
+ sender.sendMessage(command.getUsage().replace("", commandName));
+
+ List aliases = command.getAliases();
+ if (!aliases.isEmpty()) {
+ // Assemble alias list
+ StringJoiner aliasJoiner = new StringJoiner(", ", " (aliases: ", ")");
+ for (String alias : aliases) {
+ aliasJoiner.add(alias);
+ }
+
+ // Send all aliases
+ sender.sendMessage(aliasJoiner.toString());
+ }
+
+ }
+ }
+
+ @Override
+ protected @Nullable OfflinePlayer getTarget(@NotNull String identifier) {
+ return playerLoader.match(identifier);
+ }
+
+ @Override
+ protected boolean deniedCommand(@NotNull CommandSender sender, @NotNull Player onlineTarget, boolean accessInv) {
+ if (onlineTarget.equals(sender)) {
+ // Permission for opening own inventory.
+ if (!(accessInv ? Permissions.INVENTORY_OPEN_SELF : Permissions.ENDERCHEST_OPEN_SELF).hasPermission(sender)) {
+ lang.sendMessage(sender, "messages.error.permissionOpenSelf");
+ return true;
+
+ }
+ } else {
+ // Permission for opening others' inventories.
+ if (!(accessInv ? Permissions.INVENTORY_OPEN_OTHER : Permissions.ENDERCHEST_OPEN_OTHER).hasPermission(sender)) {
+ lang.sendMessage(sender, "messages.error.permissionOpenOther");
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void handle(
+ @NotNull CommandSender sender,
+ @NotNull PlayerAccess playerAccess,
+ boolean accessInv,
+ @NotNull String @NotNull [] args
+ ) {
+ Player player = (Player) sender;
+ Player target = playerAccess.player();
+ if (!config.doesNoArgsOpenSelf()) {
+ // Record the target
+ (accessInv ? this.openInvHistory : this.openEnderHistory).put(player, target.getUniqueId().toString());
+ }
+
+ // Create the inventory
+ final ISpecialInventory inv;
+ try {
+ inv = accessInv ? manager.getInventory(target) : manager.getEnderChest(target);
+ } catch (Exception e) {
+ lang.sendMessage(player, "messages.error.commandException");
+ plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e);
+ return;
+ }
+
+ // Open the inventory
+ plugin.openInventory(player, inv, playerAccess.viewOnly());
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java b/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java
new file mode 100644
index 00000000..9081ad1f
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java
@@ -0,0 +1,316 @@
+package com.lishid.openinv.command;
+
+import com.lishid.openinv.OpenInv;
+import com.lishid.openinv.util.AccessEqualMode;
+import com.lishid.openinv.util.Permissions;
+import com.lishid.openinv.util.PlayerLoader;
+import com.lishid.openinv.util.TabCompleter;
+import com.lishid.openinv.util.config.Config;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import me.nahu.scheduler.wrapper.runnable.WrappedRunnable;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A command abstraction for performing actions after looking up and loading a player.
+ */
+public abstract class PlayerLookupCommand implements TabExecutor {
+
+ protected final @NotNull OpenInv plugin;
+ protected final @NotNull LanguageManager lang;
+ protected final @NotNull Config config;
+ protected final @NotNull PlayerLoader playerLoader;
+
+ public PlayerLookupCommand(
+ @NotNull OpenInv plugin,
+ @NotNull LanguageManager lang,
+ @NotNull Config config,
+ @NotNull PlayerLoader playerLoader
+ ) {
+ this.plugin = plugin;
+ this.lang = lang;
+ this.config = config;
+ this.playerLoader = playerLoader;
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String @NotNull [] args
+ ) {
+
+ // Inventory or ender chest?
+ boolean accessInv = isAccessInventory(command);
+
+ // Get target identifier from parameters.
+ String targetId = getTargetIdentifer(sender, command, args.length > 0 ? args[0] : null, accessInv);
+ if (targetId == null) {
+ return true;
+ }
+
+ new WrappedRunnable() {
+ @Override
+ public void run() {
+ // Get target from identifier.
+ final OfflinePlayer target = getTarget(targetId);
+
+ if (target == null || (!target.hasPlayedBefore() && !target.isOnline())) {
+ lang.sendMessage(sender, "messages.error.invalidPlayer");
+ return;
+ }
+
+ new WrappedRunnable() {
+ @Override
+ public void run() {
+ // Ensure sender still exists.
+ if ((sender instanceof Player player) && !player.isValid()) {
+ return;
+ }
+
+ // Perform access checks and load target if necessary.
+ PlayerAccess onlineTarget = access(sender, target, accessInv);
+
+ if (onlineTarget != null) {
+ handle(sender, onlineTarget, accessInv, args);
+ }
+ }
+ }.runTask(PlayerLookupCommand.this.plugin);
+
+ }
+ }.runTaskAsynchronously(this.plugin);
+
+ return true;
+ }
+
+ /**
+ * Get whether a player inventory or ender chest is accessed by the {@link Command} executed.
+ *
+ * @param command the {@code Command} being executed
+ * @return {@code true} if the command is for inventories, {@code false} for ender chests
+ */
+ protected abstract boolean isAccessInventory(@NotNull Command command);
+
+ /**
+ * Determine the target identifier from the first command argument.
+ *
+ *
Implementation note: a return value of {@code null} will cause the command to cease
+ * execution with no feedback. Appropriate feedback should be sent in the implementation.
+ *
+ * @param sender the sender of the command
+ * @param argument the argument, or {@code null} if none provided
+ * @param accessInv {@code true} if an inventory is being accessed, {@code false} for ender chest
+ * @return an updated target identifier or {@code null} if no target is available
+ */
+ protected abstract @Nullable String getTargetIdentifer(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @Nullable String argument,
+ boolean accessInv
+ );
+
+ /**
+ * Get an {@link OfflinePlayer} by identifier.
+ *
+ * @param identifier the identifier
+ * @return the corresponding player or {@code null} if no match was found
+ */
+ protected abstract @Nullable OfflinePlayer getTarget(@NotNull String identifier);
+
+ /**
+ * Attempt to access the target as an online player. Performs feedback in the event of denial.
+ *
+ * @param sender the {@link CommandSender} attempting access
+ * @param target the {@link OfflinePlayer} being targeted by the command
+ * @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest
+ * @return the {@link Player} loaded or {@code null} if target is not accessible
+ */
+ protected @Nullable PlayerAccess access(
+ @NotNull CommandSender sender,
+ @NotNull OfflinePlayer target,
+ boolean invPerms
+ ) {
+ // Attempt to load online player dependent on permissions and configuration.
+ Player onlineTarget = accessAsPlayer(sender, target);
+
+ if (onlineTarget == null) {
+ return null;
+ }
+
+ // Permissions checks.
+ if (deniedCommand(sender, onlineTarget, invPerms)) {
+ return null;
+ }
+
+ return accessGeneralized(sender, onlineTarget, invPerms);
+ }
+
+ /**
+ * Helper for accessing target as an online {@link Player}. Performs checks
+ * and feedback for configuration and online/offline permissions.
+ *
+ * @param sender the {@link CommandSender} attempting access
+ * @param target the {@link OfflinePlayer} being targeted by the command
+ * @return the {@link Player} loaded or {@code null} if target is not accessible
+ */
+ protected @Nullable Player accessAsPlayer(@NotNull CommandSender sender, @NotNull OfflinePlayer target) {
+ Player onlineTarget;
+
+ if (!target.isOnline()) {
+ if (!config.isOfflineDisabled() && Permissions.ACCESS_OFFLINE.hasPermission(sender)) {
+ // Try loading the player's data.
+ onlineTarget = playerLoader.load(target);
+ } else {
+ lang.sendMessage(sender, "messages.error.permissionPlayerOffline");
+ return null;
+ }
+ } else {
+ if (Permissions.ACCESS_ONLINE.hasPermission(sender)) {
+ onlineTarget = target.getPlayer();
+ } else {
+ lang.sendMessage(sender, "messages.error.permissionPlayerOnline");
+ return null;
+ }
+ }
+
+ if (onlineTarget == null) {
+ lang.sendMessage(sender, "messages.error.invalidPlayer");
+ return null;
+ }
+
+ return onlineTarget;
+ }
+
+ /**
+ * Check for a lack of permissions related to the specific command being executed for the sender.
+ * For example, {@link Permissions#INVENTORY_OPEN_OTHER} might be required if the target and sender differ.
+ *
+ * @param sender the {@link CommandSender} attempting access
+ * @param onlineTarget the {@link Player} being targeted by the command
+ * @param accessInv {@code true} to use inventory permissions, {@code false} for ender chest
+ * @return {@code true} if the sender does not have the correct execution-specific permission
+ */
+ protected abstract boolean deniedCommand(
+ @NotNull CommandSender sender,
+ @NotNull Player onlineTarget,
+ boolean accessInv
+ );
+
+ /**
+ * Check for generalized permissions for accessing the target.
+ * By default, this is access levels and cross-world restrictions.
+ *
+ * @param sender the {@link CommandSender} attempting access
+ * @param onlineTarget the {@link Player} being targeted by the command
+ * @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest
+ * @return a {@link PlayerAccess} containing the accessed player and view mode, or {@code null} if denied
+ */
+ protected @Nullable PlayerAccess accessGeneralized(
+ @NotNull CommandSender sender,
+ @NotNull Player onlineTarget,
+ boolean invPerms
+ ) {
+
+ boolean ownContainer = sender.equals(onlineTarget);
+ Permissions edit;
+ if (invPerms) {
+ edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER;
+ } else {
+ edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_EDIT_OTHER;
+ }
+
+ boolean viewOnly = !edit.hasPermission(sender);
+
+ if (ownContainer) {
+ // Skip other access checks for self.
+ return new PlayerAccess(onlineTarget, viewOnly);
+ }
+
+ // Crossworld check
+ if (sender instanceof Player player
+ && !Permissions.ACCESS_CROSSWORLD.hasPermission(sender)
+ && !onlineTarget.getWorld().equals(player.getWorld())) {
+ lang.sendMessage(
+ sender,
+ "messages.error.permissionCrossWorld",
+ new Replacement("%target%", onlineTarget.getDisplayName())
+ );
+ return null;
+ }
+
+ AccessEqualMode accessMode = AccessEqualMode.getByPerm(sender, config);
+
+ for (int level = 4; level > 0; --level) {
+ String permission = "openinv.access.level." + level;
+ // If the target doesn't have this access level...
+ if (!onlineTarget.hasPermission(permission)) {
+ // If the viewer does have the access level, all good.
+ if (sender.hasPermission(permission)) {
+ break;
+ }
+ // Otherwise check next access level.
+ continue;
+ }
+
+ // If the viewer doesn't have an equal access level or equal access is a denial, deny.
+ if (!sender.hasPermission(permission) || accessMode == AccessEqualMode.DENY) {
+ lang.sendMessage(
+ sender,
+ "messages.error.permissionExempt",
+ new Replacement("%target%", onlineTarget.getDisplayName())
+ );
+ return null;
+ }
+
+ // Since this is a tie, setting decides view state.
+ if (accessMode == AccessEqualMode.VIEW) {
+ viewOnly = true;
+ }
+ break;
+ }
+
+ return new PlayerAccess(onlineTarget, viewOnly);
+ }
+
+ /**
+ * Perform main command functionality.
+ *
+ * @param sender the {@link CommandSender} executing the command
+ * @param target the {@link Player} being targeted
+ * @param accessInv {@code true} if an inventory is being accessed, {@code false} for ender chest
+ * @param args the original command arguments
+ */
+ protected abstract void handle(
+ @NotNull CommandSender sender,
+ @NotNull PlayerAccess target,
+ boolean accessInv,
+ @NotNull String @NotNull [] args
+ );
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!command.testPermissionSilent(sender) || args.length != 1) {
+ return Collections.emptyList();
+ }
+
+ return TabCompleter.completeOnlinePlayer(sender, args[0]);
+ }
+
+ protected record PlayerAccess(Player player, boolean viewOnly) {}
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java
new file mode 100644
index 00000000..1b8eb1b2
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java
@@ -0,0 +1,159 @@
+/*
+ * 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.command;
+
+import com.lishid.openinv.util.TabCompleter;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import com.lishid.openinv.util.SearchHelper;
+import org.bukkit.Chunk;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.BlockState;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Command for searching containers in a radius of chunks.
+ */
+public class SearchContainerCommand implements TabExecutor {
+
+ private final @NotNull Plugin plugin;
+ private final @NotNull LanguageManager lang;
+
+ public SearchContainerCommand(@NotNull Plugin plugin, @NotNull LanguageManager lang) {
+ this.plugin = plugin;
+ this.lang = lang;
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!(sender instanceof Player senderPlayer)) {
+ lang.sendMessage(sender, "messages.error.consoleUnsupported");
+ return true;
+ }
+
+ if (args.length < 1) {
+ // Must supply material
+ return false;
+ }
+
+ Material material = Material.matchMaterial(args[0]);
+
+ if (material == null) {
+ lang.sendMessage(
+ sender,
+ "messages.error.invalidMaterial",
+ new Replacement("%target%", args[0])
+ );
+ return false;
+ }
+
+ int radius = 5;
+
+ if (args.length > 1) {
+ try {
+ radius = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ // Invalid radius supplied
+ return false;
+ }
+ }
+
+ // Clamp radius.
+ int configMax = plugin.getConfig().getInt("settings.command.searchcontainer.max-radius", 10);
+ radius = Math.max(0, Math.min(radius, configMax));
+
+ World world = senderPlayer.getWorld();
+ Chunk centerChunk = senderPlayer.getLocation().getChunk();
+ StringBuilder locations = new StringBuilder();
+
+ for (int dX = -radius; dX <= radius; ++dX) {
+ for (int dZ = -radius; dZ <= radius; ++dZ) {
+ if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) {
+ continue;
+ }
+ Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ);
+ for (BlockState tileEntity : chunk.getTileEntities()) {
+ if (!(tileEntity instanceof InventoryHolder holder)) {
+ continue;
+ }
+ if (!SearchHelper.findMatch(holder.getInventory(), itemStack -> itemStack.getType() == material)) {
+ continue;
+ }
+ locations.append(holder.getInventory().getType().name().toLowerCase(Locale.ENGLISH)).append(" (")
+ .append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',')
+ .append(tileEntity.getZ()).append("), ");
+ }
+ }
+ }
+
+ // Matches found, delete trailing comma and space
+ if (!locations.isEmpty()) {
+ locations.delete(locations.length() - 2, locations.length());
+ } else {
+ lang.sendMessage(
+ sender,
+ "messages.info.container.noMatches",
+ new Replacement("%target%", material.name())
+ );
+ return true;
+ }
+
+ lang.sendMessage(
+ sender,
+ "messages.info.container.matches",
+ new Replacement("%target%", material.name()),
+ new Replacement("%detail%", locations.toString())
+ );
+ return true;
+ }
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ String[] args
+ ) {
+ if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
+ return Collections.emptyList();
+ }
+
+ String argument = args[args.length - 1];
+ if (args.length == 1) {
+ return TabCompleter.completeEnum(argument, Material.class);
+ } else {
+ return TabCompleter.completeInteger(argument);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java
new file mode 100644
index 00000000..1844c4ea
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java
@@ -0,0 +1,186 @@
+/*
+ * 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.command;
+
+import com.lishid.openinv.util.TabCompleter;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import com.lishid.openinv.util.SearchHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Registry;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Command adding the ability to search online players' inventories for enchantments of a specific
+ * type at or above the level specified.
+ *
+ * @author Jikoo
+ */
+public class SearchEnchantCommand implements TabExecutor {
+
+ private final @NotNull LanguageManager lang;
+
+ public SearchEnchantCommand(@NotNull LanguageManager lang) {
+ this.lang = lang;
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (args.length == 0) {
+ return false;
+ }
+
+ Enchantment enchant = null;
+ int level = 0;
+
+ for (String argument : args) {
+ try {
+ level = Integer.parseInt(argument);
+ continue;
+ } catch (NumberFormatException ignored) {
+ // Not a level being specified.
+ }
+
+ argument = argument.toLowerCase(Locale.ENGLISH);
+ NamespacedKey key = NamespacedKey.fromString(argument);
+ if (key == null) {
+ continue;
+ }
+
+ Enchantment localEnchant = Registry.ENCHANTMENT.get(key);
+ if (localEnchant != null) {
+ enchant = localEnchant;
+ }
+ }
+
+ // Arguments not set correctly
+ if (level == 0 && enchant == null) {
+ return false;
+ }
+
+ StringBuilder players = new StringBuilder();
+ for (Player player : Bukkit.getServer().getOnlinePlayers()) {
+ boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level);
+ boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level);
+
+ // No matches, continue
+ if (!flagInventory && !flagEnder) {
+ continue;
+ }
+
+ // Matches, append details
+ players.append(player.getName()).append(" (");
+ if (flagInventory) {
+ players.append("inv");
+ }
+ if (flagEnder) {
+ if (flagInventory) {
+ players.append(',');
+ }
+ players.append("ender");
+ }
+ players.append("), ");
+ }
+
+ if (!players.isEmpty()) {
+ // Matches found, delete trailing comma and space
+ players.delete(players.length() - 2, players.length());
+ } else {
+ lang.sendMessage(
+ sender,
+ "messages.info.player.noMatches",
+ new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level)
+ );
+ return true;
+ }
+
+ lang.sendMessage(
+ sender,
+ "messages.info.player.matches",
+ new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level),
+ new Replacement("%detail%", players.toString())
+ );
+ return true;
+ }
+
+ private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) {
+ return SearchHelper.findMatch(
+ inventory,
+ itemStack -> {
+ // Ensure meta is available and has enchantments.
+ if (!itemStack.hasItemMeta()) {
+ return false;
+ }
+ ItemMeta meta = itemStack.getItemMeta();
+ if (meta == null || !meta.hasEnchants()) {
+ return false;
+ }
+
+ // If enchantment is provided, use it.
+ if (enchant != null) {
+ return meta.getEnchantLevel(enchant) >= minLevel;
+ }
+
+ // Otherwise, check all enchantment levels.
+ for (int enchLevel : meta.getEnchants().values()) {
+ if (enchLevel >= minLevel) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ );
+ }
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) {
+ return Collections.emptyList();
+ }
+
+ if (args.length == 1) {
+ return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Registry.ENCHANTMENT.stream().toArray(Enchantment[]::new));
+ } else {
+ return TabCompleter.completeInteger(args[1]);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java
new file mode 100644
index 00000000..c2a5a32d
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java
@@ -0,0 +1,143 @@
+/*
+ * 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.command;
+
+import com.lishid.openinv.util.TabCompleter;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.lang.Replacement;
+import com.lishid.openinv.util.SearchHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabExecutor;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class SearchInvCommand implements TabExecutor {
+
+ private final @NotNull LanguageManager lang;
+
+ public SearchInvCommand(@NotNull LanguageManager lang) {
+ this.lang = lang;
+ }
+
+ @Override
+ public boolean onCommand(
+ @NotNull CommandSender sender,
+ @NotNull Command command,
+ @NotNull String label,
+ @NotNull String[] args
+ ) {
+
+ Material material = null;
+
+ if (args.length >= 1) {
+ material = Material.matchMaterial(args[0]);
+ }
+
+ if (material == null) {
+ lang.sendMessage(
+ sender,
+ "messages.error.invalidMaterial",
+ new Replacement("%target%", args.length > 0 ? args[0] : "null")
+ );
+ return false;
+ }
+
+ int count = 1;
+
+ if (args.length >= 2) {
+ try {
+ count = Integer.parseInt(args[1]);
+ } catch (NumberFormatException ex) {
+ lang.sendMessage(
+ sender,
+ "messages.error.invalidNumber",
+ new Replacement("%target%", args[1])
+ );
+ return false;
+ }
+ }
+
+ StringBuilder players = new StringBuilder();
+ boolean searchInv = command.getName().equals("searchinv");
+ for (Player player : Bukkit.getServer().getOnlinePlayers()) {
+ Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest();
+ if (findMatch(inventory, material, count)) {
+ players.append(player.getName()).append(", ");
+ break;
+ }
+ }
+
+ // Matches found, delete trailing comma and space
+ if (!players.isEmpty()) {
+ players.delete(players.length() - 2, players.length());
+ } else {
+ lang.sendMessage(
+ sender,
+ "messages.info.player.noMatches",
+ new Replacement("%target%", material.name())
+ );
+ return true;
+ }
+
+ lang.sendMessage(
+ sender,
+ "messages.info.player.matches",
+ new Replacement("%target%", material.name()),
+ new Replacement("%detail%", players.toString())
+ );
+ return true;
+ }
+
+ private boolean findMatch(@NotNull Inventory inventory, @NotNull Material material, int count) {
+ AtomicInteger total = new AtomicInteger();
+ return SearchHelper.findMatch(
+ inventory,
+ itemStack -> {
+ if (itemStack.getType() == material && itemStack.getAmount() > 0) {
+ return total.addAndGet(itemStack.getAmount()) >= count;
+ }
+ return false;
+ }
+ );
+ }
+
+ @Override
+ public List onTabComplete(
+ @NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
+ @NotNull String[] args
+ ) {
+ if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
+ return Collections.emptyList();
+ }
+
+ String argument = args[args.length - 1];
+ if (args.length == 1) {
+ return TabCompleter.completeEnum(argument, Material.class);
+ } else {
+ return TabCompleter.completeInteger(argument);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java
deleted file mode 100644
index 1a765432..00000000
--- a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.commands;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.util.TabCompleter;
-import java.util.Collections;
-import java.util.List;
-import java.util.function.BiConsumer;
-import java.util.function.Predicate;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-public class ContainerSettingCommand implements TabExecutor {
-
- private final OpenInv plugin;
-
- public ContainerSettingCommand(final OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!(sender instanceof Player)) {
- plugin.sendMessage(sender, "messages.error.consoleUnsupported");
- return true;
- }
-
- Player player = (Player) sender;
- boolean any = command.getName().startsWith("any");
- Predicate getSetting = any ? plugin::getPlayerAnyChestStatus : plugin::getPlayerSilentChestStatus;
- BiConsumer setSetting = any ? plugin::setPlayerAnyChestStatus : plugin::setPlayerSilentChestStatus;
-
- if (args.length > 0) {
- args[0] = args[0].toLowerCase();
-
- if (args[0].equals("on")) {
- setSetting.accept(player, true);
- } else if (args[0].equals("off")) {
- setSetting.accept(player, false);
- } else if (!args[0].equals("check")) {
- // Invalid argument, show usage.
- return false;
- }
-
- } else {
- setSetting.accept(player, !getSetting.test(player));
- }
-
- String onOff = plugin.getLocalizedMessage(player, getSetting.test(player) ? "messages.info.on" : "messages.info.off");
- if (onOff == null) {
- onOff = String.valueOf(getSetting.test(player));
- }
-
- plugin.sendMessage(sender, "messages.info.settingState","%setting%", any ? "AnyContainer" : "SilentContainer", "%state%", onOff);
-
- return true;
- }
-
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!command.testPermissionSilent(sender) || args.length != 1) {
- return Collections.emptyList();
- }
-
- return TabCompleter.completeString(args[0], new String[] {"check", "on", "off"});
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java
deleted file mode 100644
index 4571df81..00000000
--- a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.commands;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.internal.ISpecialInventory;
-import com.lishid.openinv.util.Permissions;
-import com.lishid.openinv.util.TabCompleter;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import org.bukkit.OfflinePlayer;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.jetbrains.annotations.NotNull;
-
-public class OpenInvCommand implements TabExecutor {
-
- private final OpenInv plugin;
- private final HashMap openInvHistory = new HashMap<>();
- private final HashMap openEnderHistory = new HashMap<>();
-
- public OpenInvCommand(final OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @Override
- public boolean onCommand(@NotNull final CommandSender sender, @NotNull final Command command, @NotNull final String label, @NotNull final String[] args) {
- if (!(sender instanceof Player)) {
- plugin.sendMessage(sender, "messages.error.consoleUnsupported");
- return true;
- }
-
- if (args.length > 0 && (args[0].equalsIgnoreCase("help") || args[0].equals("?"))) {
- this.plugin.showHelp((Player) sender);
- return true;
- }
-
- final Player player = (Player) sender;
- final boolean openinv = command.getName().equals("openinv");
-
- // History management
- String history = (openinv ? this.openInvHistory : this.openEnderHistory).get(player);
-
- if (history == null || history.isEmpty()) {
- history = player.getName();
- (openinv ? this.openInvHistory : this.openEnderHistory).put(player, history);
- }
-
- final String name;
-
- // Read from history if target is not named
- if (args.length < 1) {
- name = history;
- } else {
- name = args[0];
- }
-
- new BukkitRunnable() {
- @Override
- public void run() {
- final OfflinePlayer offlinePlayer = OpenInvCommand.this.plugin.matchPlayer(name);
-
- if (offlinePlayer == null || !offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline()) {
- plugin.sendMessage(player, "messages.error.invalidPlayer");
- return;
- }
-
- new BukkitRunnable() {
- @Override
- public void run() {
- if (!player.isOnline()) {
- return;
- }
- OpenInvCommand.this.openInventory(player, offlinePlayer, openinv);
- }
- }.runTask(OpenInvCommand.this.plugin);
-
- }
- }.runTaskAsynchronously(this.plugin);
-
- return true;
- }
-
- private void openInventory(final Player player, final OfflinePlayer target, boolean openinv) {
- Player onlineTarget;
- boolean online = target.isOnline();
-
- if (!online) {
- if (Permissions.OPENOFFLINE.hasPermission(player)) {
- // Try loading the player's data
- onlineTarget = this.plugin.loadPlayer(target);
- } else {
- plugin.sendMessage(player, "messages.error.permissionPlayerOffline");
- return;
- }
- } else {
- if (Permissions.OPENONLINE.hasPermission(player)) {
- onlineTarget = target.getPlayer();
- } else {
- plugin.sendMessage(player, "messages.error.permissionPlayerOnline");
- return;
- }
- }
-
- if (onlineTarget == null) {
- plugin.sendMessage(player, "messages.error.invalidPlayer");
- return;
- }
-
- // Permissions checks
- if (onlineTarget.equals(player)) {
- // Inventory: Additional permission required to open own inventory
- if (openinv && !Permissions.OPENSELF.hasPermission(player)) {
- plugin.sendMessage(player, "messages.error.permissionOpenSelf");
- return;
- }
- } else {
- // Enderchest: Additional permission required to open others' ender chests
- if (!openinv && !Permissions.ENDERCHEST_ALL.hasPermission(player)) {
- plugin.sendMessage(player, "messages.error.permissionEnderAll");
- return;
- }
-
- // Protected check
- if (!Permissions.OVERRIDE.hasPermission(player)
- && Permissions.EXEMPT.hasPermission(onlineTarget)) {
- plugin.sendMessage(player, "messages.error.permissionExempt",
- "%target%", onlineTarget.getDisplayName());
- return;
- }
-
- // Crossworld check
- if (!Permissions.CROSSWORLD.hasPermission(player)
- && !onlineTarget.getWorld().equals(player.getWorld())) {
- plugin.sendMessage(player, "messages.error.permissionCrossWorld",
- "%target%", onlineTarget.getDisplayName());
- return;
- }
- }
-
- // Record the target
- (openinv ? this.openInvHistory : this.openEnderHistory).put(player, this.plugin.getPlayerID(target));
-
- // Create the inventory
- final ISpecialInventory inv;
- try {
- inv = openinv ? this.plugin.getSpecialInventory(onlineTarget, online) : this.plugin.getSpecialEnderChest(onlineTarget, online);
- } catch (Exception e) {
- plugin.sendMessage(player, "messages.error.commandException");
- e.printStackTrace();
- return;
- }
-
- // Open the inventory
- plugin.openInventory(player, inv);
- }
-
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!command.testPermissionSilent(sender) || args.length != 1) {
- return Collections.emptyList();
- }
-
- return TabCompleter.completeOnlinePlayer(sender, args[0]);
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
deleted file mode 100644
index 7242ee94..00000000
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.commands;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.util.TabCompleter;
-import java.util.Collections;
-import java.util.List;
-import org.bukkit.Chunk;
-import org.bukkit.Material;
-import org.bukkit.World;
-import org.bukkit.block.BlockState;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryHolder;
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Command for searching containers in a radius of chunks.
- */
-public class SearchContainerCommand implements TabExecutor {
-
- private final OpenInv plugin;
-
- public SearchContainerCommand(OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!(sender instanceof Player)) {
- plugin.sendMessage(sender, "messages.error.consoleUnsupported");
- return true;
- }
-
- if (args.length < 1) {
- // Must supply material
- return false;
- }
-
- Material material = Material.getMaterial(args[0].toUpperCase());
-
- if (material == null) {
- plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args[0]);
- return false;
- }
-
- int radius = 5;
-
- if (args.length > 1) {
- try {
- radius = Integer.parseInt(args[1]);
- } catch (NumberFormatException e) {
- // Invalid radius supplied
- return false;
- }
- }
-
- Player senderPlayer = (Player) sender;
- World world = senderPlayer.getWorld();
- Chunk centerChunk = senderPlayer.getLocation().getChunk();
- StringBuilder locations = new StringBuilder();
-
- for (int dX = -radius; dX <= radius; ++dX) {
- for (int dZ = -radius; dZ <= radius; ++dZ) {
- if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) {
- continue;
- }
- Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ);
- for (BlockState tileEntity : chunk.getTileEntities()) {
- if (!(tileEntity instanceof InventoryHolder)) {
- continue;
- }
- InventoryHolder holder = (InventoryHolder) tileEntity;
- if (!holder.getInventory().contains(material)) {
- continue;
- }
- locations.append(holder.getInventory().getType().name().toLowerCase()).append(" (")
- .append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',')
- .append(tileEntity.getZ()).append("), ");
- }
- }
- }
-
- // Matches found, delete trailing comma and space
- if (locations.length() > 0) {
- locations.delete(locations.length() - 2, locations.length());
- } else {
- plugin.sendMessage(sender, "messages.info.container.noMatches",
- "%target%", material.name());
- return true;
- }
-
- plugin.sendMessage(sender, "messages.info.container.matches",
- "%target%", material.name(), "%detail%", locations.toString());
- return true;
- }
-
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
- if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
- return Collections.emptyList();
- }
-
- String argument = args[args.length - 1];
- if (args.length == 1) {
- return TabCompleter.completeEnum(argument, Material.class);
- } else {
- return TabCompleter.completeInteger(argument);
- }
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
deleted file mode 100644
index 7f5eca95..00000000
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.commands;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.util.TabCompleter;
-import java.util.Collections;
-import java.util.List;
-import org.bukkit.Material;
-import org.bukkit.NamespacedKey;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.enchantments.Enchantment;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Command adding the ability to search online players' inventories for enchantments of a specific
- * type at or above the level specified.
- *
- * @author Jikoo
- */
-public class SearchEnchantCommand implements TabExecutor {
-
- private final OpenInv plugin;
-
- public SearchEnchantCommand(OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (args.length == 0) {
- return false;
- }
-
- Enchantment enchant = null;
- int level = 0;
-
- for (String argument : args) {
- try {
- level = Integer.parseInt(argument);
- continue;
- } catch (NumberFormatException ignored) {}
-
- argument = argument.toLowerCase();
- int colon = argument.indexOf(':');
- NamespacedKey key;
- try {
- if (colon > -1 && colon < argument.length() - 1) {
- key = new NamespacedKey(argument.substring(0, colon), argument.substring(colon + 1));
- } else {
- key = NamespacedKey.minecraft(argument);
- }
- } catch (IllegalArgumentException ignored) {
- continue;
- }
-
- Enchantment localEnchant = Enchantment.getByKey(key);
- if (localEnchant != null) {
- enchant = localEnchant;
- }
- }
-
- // Arguments not set correctly
- if (level == 0 && enchant == null) {
- return false;
- }
-
- StringBuilder players = new StringBuilder();
- for (Player player : plugin.getServer().getOnlinePlayers()) {
- boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level);
- boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level);
-
- // No matches, continue
- if (!flagInventory && !flagEnder) {
- continue;
- }
-
- // Matches, append details
- players.append(player.getName()).append(" (");
- if (flagInventory) {
- players.append("inv");
- }
- if (flagEnder) {
- if (flagInventory) {
- players.append(',');
- }
- players.append("ender");
- }
- players.append("), ");
- }
-
- if (players.length() > 0) {
- // Matches found, delete trailing comma and space
- players.delete(players.length() - 2, players.length());
- } else {
- plugin.sendMessage(sender, "messages.info.player.noMatches",
- "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level);
- return true;
- }
-
- plugin.sendMessage(sender, "messages.info.player.matches",
- "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level,
- "%detail%", players.toString());
- return true;
- }
-
- private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) {
- for (ItemStack item : inventory.getContents()) {
- //noinspection ConstantConditions // Spigot improperly annotated, should be ItemStack @NotNull []
- if (item == null || item.getType() == Material.AIR) {
- continue;
- }
- if (enchant != null) {
- if (item.containsEnchantment(enchant) && item.getEnchantmentLevel(enchant) >= minLevel) {
- return true;
- }
- } else {
- if (!item.hasItemMeta()) {
- continue;
- }
- ItemMeta meta = item.getItemMeta();
- if (meta == null || !meta.hasEnchants()) {
- continue;
- }
- for (int enchLevel : meta.getEnchants().values()) {
- if (enchLevel >= minLevel) {
- return true;
- }
- }
- }
- }
- return false;
- }
-
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) {
- return Collections.emptyList();
- }
-
- if (args.length == 1) {
- return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Enchantment.values());
- } else {
- return TabCompleter.completeInteger(args[1]);
- }
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
deleted file mode 100644
index c471b30f..00000000
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.commands;
-
-import com.lishid.openinv.OpenInv;
-import com.lishid.openinv.util.TabCompleter;
-import java.util.Collections;
-import java.util.List;
-import org.bukkit.Material;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.Inventory;
-import org.jetbrains.annotations.NotNull;
-
-public class SearchInvCommand implements TabExecutor {
-
- private final OpenInv plugin;
-
- public SearchInvCommand(OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @Override
- public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
-
- Material material = null;
-
- if (args.length >= 1) {
- material = Material.getMaterial(args[0].toUpperCase());
- }
-
- if (material == null) {
- plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args.length > 0 ? args[0] : "null");
- return false;
- }
-
- int count = 1;
-
- if (args.length >= 2) {
- try {
- count = Integer.parseInt(args[1]);
- } catch (NumberFormatException ex) {
- plugin.sendMessage(sender, "messages.error.invalidNumber", "%target%", args[1]);
- return false;
- }
- }
-
- StringBuilder players = new StringBuilder();
- boolean searchInv = command.getName().equals("searchinv");
- for (Player player : plugin.getServer().getOnlinePlayers()) {
- Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest();
- if (inventory.contains(material, count)) {
- players.append(player.getName()).append(", ");
- }
- }
-
- // Matches found, delete trailing comma and space
- if (players.length() > 0) {
- players.delete(players.length() - 2, players.length());
- } else {
- plugin.sendMessage(sender, "messages.info.player.noMatches",
- "%target%", material.name());
- return true;
- }
-
- plugin.sendMessage(sender, "messages.info.player.matches",
- "%target%", material.name(), "%detail%", players.toString());
- return true;
- }
-
- @Override
- public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
- if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
- return Collections.emptyList();
- }
-
- String argument = args[args.length - 1];
- if (args.length == 1) {
- return TabCompleter.completeEnum(argument, Material.class);
- } else {
- return TabCompleter.completeInteger(argument);
- }
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java b/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java
deleted file mode 100644
index 1a81491f..00000000
--- a/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.internal;
-
-import org.bukkit.OfflinePlayer;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.InventoryView;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-public interface IPlayerDataManager {
-
- /**
- * Loads a Player for an OfflinePlayer.
- *
- * This method is potentially blocking, and should not be called on the main thread.
- *
- * @param offline the OfflinePlayer
- * @return the Player loaded
- */
- @Nullable Player loadPlayer(@NotNull OfflinePlayer offline);
-
- /**
- * Creates a new Player from an existing one that will function slightly better offline.
- *
- * @return the Player
- */
- @NotNull Player inject(@NotNull Player player);
-
- /**
- * Opens an ISpecialInventory for a Player.
- *
- * @param player the Player opening the ISpecialInventory
- * @param inventory the Inventory
- *`
- * @return the InventoryView opened
- */
- @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
-
- /**
- * Convert a raw slot number into a player inventory slot number.
- *
- *
Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular
- * player inventory slot number.
- *
- * @param view the open inventory view
- * @param rawSlot the raw slot in the view
- * @return the converted slot number
- */
- int convertToPlayerSlot(InventoryView view, int rawSlot);
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java b/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java
deleted file mode 100644
index 8fc6f09d..00000000
--- a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java
+++ /dev/null
@@ -1,83 +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.internal;
-
-import com.lishid.openinv.OpenInv;
-import org.bukkit.entity.HumanEntity;
-import org.bukkit.entity.Player;
-import org.bukkit.event.inventory.InventoryType;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.InventoryView;
-import org.jetbrains.annotations.NotNull;
-
-public class OpenInventoryView extends InventoryView {
-
- private final Player player;
- private final ISpecialInventory inventory;
- private final String titleKey;
- private final String titleDefaultSuffix;
- private String title;
-
- public OpenInventoryView(Player player, ISpecialInventory inventory, String titleKey, String titleDefaultSuffix) {
- this.player = player;
- this.inventory = inventory;
- this.titleKey = titleKey;
- this.titleDefaultSuffix = titleDefaultSuffix;
- }
-
- @Override
- public @NotNull Inventory getTopInventory() {
- return inventory.getBukkitInventory();
- }
-
- @Override
- public @NotNull Inventory getBottomInventory() {
- return getPlayer().getInventory();
- }
-
- @Override
- public @NotNull HumanEntity getPlayer() {
- return player;
- }
-
- @Override
- public @NotNull InventoryType getType() {
- return inventory.getBukkitInventory().getType();
- }
-
- @Override
- public @NotNull String getTitle() {
- if (title == null) {
- HumanEntity owner = getPlayer();
-
- String localTitle = OpenInv.getPlugin(OpenInv.class)
- .getLocalizedMessage(
- owner,
- titleKey,
- "%player%",
- owner.getName());
- if (localTitle != null) {
- title = localTitle;
- } else {
- title = owner.getName() + titleDefaultSuffix;
- }
- }
-
- return title;
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java b/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java
new file mode 100644
index 00000000..ffcba7d4
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java
@@ -0,0 +1,144 @@
+/*
+ * 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.listener;
+
+import com.google.errorprone.annotations.Keep;
+import com.lishid.openinv.internal.ViewOnly;
+import com.lishid.openinv.util.InternalAccessor;
+import com.lishid.openinv.util.Permissions;
+import com.lishid.openinv.util.lang.LanguageManager;
+import com.lishid.openinv.util.setting.PlayerToggles;
+import org.bukkit.GameMode;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event.Result;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryDragEvent;
+import org.bukkit.event.inventory.InventoryInteractEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * A listener managing AnyContainer, SilentContainer, and more.
+ */
+public class ContainerListener implements Listener {
+
+ private final @NotNull InternalAccessor accessor;
+ private final @NotNull LanguageManager lang;
+
+ public ContainerListener(@NotNull InternalAccessor accessor, @NotNull LanguageManager lang) {
+ this.accessor = accessor;
+ this.lang = lang;
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ private void onPlayerInteract(@NotNull PlayerInteractEvent event) {
+ // Ignore events from other plugins.
+ if (!PlayerInteractEvent.class.equals(event.getClass())) {
+ return;
+ }
+
+ if (event.getAction() != Action.RIGHT_CLICK_BLOCK
+ || event.getPlayer().isSneaking()
+ || event.useInteractedBlock() == Result.DENY
+ || event.getClickedBlock() == null
+ || !accessor.getAnySilentContainer().isAnySilentContainer(event.getClickedBlock())) {
+ return;
+ }
+
+ Player player = event.getPlayer();
+ UUID playerId = player.getUniqueId();
+ boolean any = Permissions.CONTAINER_ANY_USE.hasPermission(player, Permissions.CONTAINER_ANY) && PlayerToggles.any().is(playerId);
+ boolean needsAny = accessor.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
+
+ if (!any && needsAny) {
+ return;
+ }
+
+ boolean silent = Permissions.CONTAINER_SILENT_USE.hasPermission(player, Permissions.CONTAINER_SILENT) && PlayerToggles.silent().is(playerId);
+
+ // If anycontainer or silentcontainer is active
+ if (any || silent) {
+ if (accessor.getAnySilentContainer().activateContainer(player, silent, event.getClickedBlock())) {
+ if (silent && needsAny) {
+ lang.sendSystemMessage(player, "messages.info.containerBlockedSilent");
+ } else if (needsAny) {
+ lang.sendSystemMessage(player, "messages.info.containerBlocked");
+ } else if (silent) {
+ lang.sendSystemMessage(player, "messages.info.containerSilent");
+ }
+ }
+ event.setCancelled(true);
+ }
+ }
+
+ @Keep
+ @EventHandler
+ private void onInventoryClose(@NotNull final InventoryCloseEvent event) {
+ if (!(event.getPlayer() instanceof Player player)) {
+ return;
+ }
+
+ InventoryHolder holder = event.getInventory().getHolder();
+ if (PlayerToggles.silent().is(player.getUniqueId())
+ && holder != null
+ && this.accessor.getAnySilentContainer().isAnySilentContainer(holder)) {
+ this.accessor.getAnySilentContainer().deactivateContainer(player);
+ }
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.LOWEST)
+ private void onInventoryClick(@NotNull final InventoryClickEvent event) {
+ handleInventoryInteract(event);
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.LOWEST)
+ private void onInventoryDrag(@NotNull final InventoryDragEvent event) {
+ handleInventoryInteract(event);
+ }
+
+ private void handleInventoryInteract(@NotNull final InventoryInteractEvent event) {
+ HumanEntity entity = event.getWhoClicked();
+
+ // Un-cancel spectator interactions.
+ if (entity.getGameMode() == GameMode.SPECTATOR && Permissions.SPECTATE_CLICK.hasPermission(entity)) {
+ event.setCancelled(false);
+ }
+
+ if (event.isCancelled()) {
+ return;
+ }
+
+ Inventory inventory = event.getView().getTopInventory();
+ if (inventory instanceof ViewOnly) {
+ event.setCancelled(true);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java b/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java
new file mode 100644
index 00000000..b39535ae
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java
@@ -0,0 +1,22 @@
+package com.lishid.openinv.listener;
+
+import com.google.errorprone.annotations.Keep;
+import com.lishid.openinv.util.setting.PlayerToggles;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+public class ToggleListener implements Listener {
+
+ @Keep
+ @EventHandler
+ private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
+ UUID playerId = event.getPlayer().getUniqueId();
+ PlayerToggles.any().set(playerId, false);
+ PlayerToggles.silent().set(playerId, false);
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java
deleted file mode 100644
index eee66861..00000000
--- a/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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 com.lishid.openinv.internal.ISpecialPlayerInventory;
-import com.lishid.openinv.util.InventoryAccess;
-import com.lishid.openinv.util.Permissions;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.bukkit.GameMode;
-import org.bukkit.entity.HumanEntity;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.inventory.InventoryAction;
-import org.bukkit.event.inventory.InventoryClickEvent;
-import org.bukkit.event.inventory.InventoryCloseEvent;
-import org.bukkit.event.inventory.InventoryDragEvent;
-import org.bukkit.event.inventory.InventoryInteractEvent;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.InventoryView;
-import org.bukkit.inventory.ItemStack;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * Listener for inventory-related events to prevent modification of inventories where not allowed.
- *
- * @author Jikoo
- */
-public class InventoryListener implements Listener {
-
- private final OpenInv plugin;
-
- public InventoryListener(final OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @EventHandler
- public void onInventoryClose(@NotNull final InventoryCloseEvent event) {
- if (!(event.getPlayer() instanceof Player)) {
- return;
- }
-
- Player player = (Player) event.getPlayer();
-
- if (this.plugin.getPlayerSilentChestStatus(player)) {
- this.plugin.getAnySilentContainer().deactivateContainer(player);
- }
- }
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onInventoryClick(@NotNull final InventoryClickEvent event) {
- if (handleInventoryInteract(event)) {
- return;
- }
-
- // Safe cast - has to be a player to be the holder of a special player inventory.
- Player player = (Player) event.getWhoClicked();
-
- if (event.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) {
- // All own-inventory interactions require updates to display properly.
- // Update in same tick after event completion.
- this.plugin.getServer().getScheduler().runTask(this.plugin, player::updateInventory);
- return;
- }
-
- // Extra handling for MOVE_TO_OTHER_INVENTORY - apparently Mojang no longer removes the item from the target
- // inventory prior to adding it to existing stacks.
- ItemStack currentItem = event.getCurrentItem();
- if (currentItem == null) {
- // Other plugin doing some sort of handling (would be NOTHING for null item otherwise), ignore.
- return;
- }
-
- ItemStack clone = currentItem.clone();
- event.setCurrentItem(null);
-
- // Complete add action in same tick after event completion.
- this.plugin.getServer().getScheduler().runTask(this.plugin, () -> {
- player.getInventory().addItem(clone);
- player.updateInventory();
- });
- }
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onInventoryDrag(@NotNull final InventoryDragEvent event) {
- if (handleInventoryInteract(event)) {
- return;
- }
-
- InventoryView view = event.getView();
- int topSize = view.getTopInventory().getSize();
-
- // Get bottom inventory active slots as player inventory slots.
- Set slots = event.getRawSlots().stream()
- .filter(slot -> slot >= topSize)
- .map(slot -> plugin.convertToPlayerSlot(view, slot)).collect(Collectors.toSet());
-
- int overlapLosses = 0;
-
- // Count overlapping slots.
- for (Map.Entry newItem : event.getNewItems().entrySet()) {
- int rawSlot = newItem.getKey();
-
- // Skip bottom inventory slots.
- if (rawSlot >= topSize) {
- continue;
- }
-
- int convertedSlot = plugin.convertToPlayerSlot(view, rawSlot);
-
- if (slots.contains(convertedSlot)) {
- overlapLosses += getCountDiff(view.getItem(rawSlot), newItem.getValue());
- }
- }
-
- // Allow no overlap to proceed as usual.
- if (overlapLosses < 1) {
- return;
- }
-
- ItemStack cursor = event.getCursor();
- if (cursor != null) {
- cursor.setAmount(cursor.getAmount() + overlapLosses);
- } else {
- cursor = event.getOldCursor().clone();
- cursor.setAmount(overlapLosses);
- }
-
- event.setCursor(cursor);
- }
-
- private int getCountDiff(@Nullable ItemStack original, @NotNull ItemStack result) {
- if (original == null || original.getType() != result.getType()) {
- return result.getAmount();
- }
-
- return result.getAmount() - original.getAmount();
- }
-
- /**
- * Handle common InventoryInteractEvent functions.
- *
- * @param event the InventoryInteractEvent
- * @return true unless the top inventory is the holder's own inventory
- */
- private boolean handleInventoryInteract(@NotNull final InventoryInteractEvent event) {
- HumanEntity entity = event.getWhoClicked();
-
- // Un-cancel spectator interactions.
- if (Permissions.SPECTATE.hasPermission(entity) && entity.getGameMode() == GameMode.SPECTATOR) {
- event.setCancelled(false);
- }
-
- if (event.isCancelled()) {
- return true;
- }
-
- Inventory inventory = event.getView().getTopInventory();
-
- // Is the inventory a special ender chest?
- if (InventoryAccess.isEnderChest(inventory)) {
- // Disallow ender chest interaction for users without edit permission.
- if (!Permissions.EDITENDER.hasPermission(entity)) {
- event.setCancelled(true);
- }
- return true;
- }
-
- ISpecialPlayerInventory playerInventory = InventoryAccess.getPlayerInventory(inventory);
-
- // Ignore inventories other than special player inventories.
- if (playerInventory == null) {
- return true;
- }
-
- // Disallow player inventory interaction for users without edit permission.
- if (!Permissions.EDITINV.hasPermission(entity)) {
- event.setCancelled(true);
- return true;
- }
-
- // Only specially handle actions in the player's own inventory.
- return !event.getWhoClicked().equals(event.getView().getTopInventory().getHolder());
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java
deleted file mode 100644
index 5f6712a4..00000000
--- a/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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 com.lishid.openinv.util.Permissions;
-import org.bukkit.entity.Player;
-import org.bukkit.event.Event.Result;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-import org.bukkit.event.block.Action;
-import org.bukkit.event.player.PlayerChangedWorldEvent;
-import org.bukkit.event.player.PlayerInteractEvent;
-import org.bukkit.event.player.PlayerJoinEvent;
-import org.bukkit.event.player.PlayerQuitEvent;
-
-public class PlayerListener implements Listener {
-
- private final OpenInv plugin;
-
- public PlayerListener(OpenInv plugin) {
- this.plugin = plugin;
- }
-
- @EventHandler(priority = EventPriority.LOWEST)
- public void onPlayerJoin(final PlayerJoinEvent event) {
- plugin.setPlayerOnline(event.getPlayer());
- }
-
- @EventHandler(priority = EventPriority.MONITOR)
- public void onPlayerQuit(PlayerQuitEvent event) {
- plugin.setPlayerOffline(event.getPlayer());
- }
-
- @EventHandler
- public void onWorldChange(PlayerChangedWorldEvent event) {
- plugin.changeWorld(event.getPlayer());
- }
-
- @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
- public void onPlayerInteract(PlayerInteractEvent event) {
-
- // Do not cancel 3rd party plugins' custom events
- if (!PlayerInteractEvent.class.equals(event.getClass())) {
- return;
- }
-
- if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getPlayer().isSneaking()
- || event.useInteractedBlock() == Result.DENY || event.getClickedBlock() == null
- || !plugin.getAnySilentContainer().isAnySilentContainer(event.getClickedBlock())) {
- return;
- }
-
- Player player = event.getPlayer();
- boolean any = Permissions.ANYCHEST.hasPermission(player) && plugin.getPlayerAnyChestStatus(player);
- boolean needsAny = plugin.getAnySilentContainer().isAnyContainerNeeded(player, event.getClickedBlock());
-
- if (!any && needsAny) {
- return;
- }
-
- boolean silent = Permissions.SILENT.hasPermission(player) && plugin.getPlayerSilentChestStatus(player);
-
- // If anycontainer or silentcontainer is active
- if (any || silent) {
- if (plugin.getAnySilentContainer().activateContainer(player, silent, event.getClickedBlock())) {
- if (silent && plugin.notifySilentChest() && needsAny && plugin.notifyAnyChest()) {
- plugin.sendSystemMessage(player, "messages.info.containerBlockedSilent");
- } else if (needsAny && plugin.notifyAnyChest()) {
- plugin.sendSystemMessage(player, "messages.info.containerBlocked");
- } else if (silent && plugin.notifySilentChest()) {
- plugin.sendSystemMessage(player, "messages.info.containerSilent");
- }
- }
- event.setCancelled(true);
- }
- }
-
-}
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 546ae261..00000000
--- a/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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/AccessEqualMode.java b/plugin/src/main/java/com/lishid/openinv/util/AccessEqualMode.java
new file mode 100644
index 00000000..a68914d9
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/AccessEqualMode.java
@@ -0,0 +1,38 @@
+package com.lishid.openinv.util;
+
+import com.lishid.openinv.util.config.Config;
+import org.bukkit.permissions.Permissible;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Locale;
+
+public enum AccessEqualMode {
+
+ DENY, ALLOW, VIEW;
+
+ public static @NotNull AccessEqualMode of(@Nullable String value) {
+ if (value == null) {
+ return VIEW;
+ }
+ return switch (value.toLowerCase(Locale.ENGLISH)) {
+ case "deny", "false" -> DENY;
+ case "allow", "true" -> ALLOW;
+ default -> VIEW;
+ };
+ }
+
+ public static @NotNull AccessEqualMode getByPerm(@NotNull Permissible permissible, @NotNull Config config) {
+ if (Permissions.ACCESS_EQUAL_EDIT.hasPermission(permissible)) {
+ return AccessEqualMode.ALLOW;
+ }
+ if (Permissions.ACCESS_EQUAL_VIEW.hasPermission(permissible)) {
+ return AccessEqualMode.VIEW;
+ }
+ if (Permissions.ACCESS_EQUAL_DENY.hasPermission(permissible)) {
+ return AccessEqualMode.DENY;
+ }
+ return config.getAccessEqualMode();
+ }
+
+}
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 fc8c60e5..00000000
--- a/plugin/src/main/java/com/lishid/openinv/util/Cache.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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
deleted file mode 100644
index faecfc8c..00000000
--- a/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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.lishid.openinv.OpenInv;
-import java.io.File;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-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 void checkForUpdates() {
- final int version = plugin.getConfig().getInt("config-version", 1);
- ConfigurationSection defaults = plugin.getConfig().getDefaults();
- if (defaults == null || version >= defaults.getInt("config-version")) {
- return;
- }
-
- plugin.getLogger().info("Configuration update found! Performing update...");
-
- // Backup the old config file
- try {
- plugin.getConfig().save(new File(plugin.getDataFolder(), "config_old.yml"));
- plugin.getLogger().info("Backed up config.yml to config_old.yml before updating.");
- } catch (IOException e) {
- plugin.getLogger().warning("Could not back up config.yml before updating!");
- }
-
- plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
- if (version < 2) {
- updateConfig1To2();
- }
- if (version < 3) {
- updateConfig2To3();
- }
- if (version < 4) {
- updateConfig3To4();
- }
-
- plugin.getServer().getScheduler().runTask(plugin, () -> {
- plugin.saveConfig();
- plugin.getLogger().info("Configuration update complete!");
- });
- });
- }
-
- private void updateConfig3To4() {
- plugin.getServer().getScheduler().runTask(plugin, () -> {
- plugin.getConfig().set("notify", null);
- plugin.getConfig().set("settings.locale", "en_US");
- plugin.getConfig().set("config-version", 4);
- });
- }
-
- private void updateConfig2To3() {
- plugin.getServer().getScheduler().runTask(plugin, () -> {
- plugin.getConfig().set("config-version", 3);
- plugin.getConfig().set("items.open-inv", null);
- plugin.getConfig().set("ItemOpenInv", null);
- plugin.getConfig().set("toggles.items.open-inv", null);
- plugin.getConfig().set("settings.disable-saving",
- plugin.getConfig().getBoolean("DisableSaving", false));
- plugin.getConfig().set("DisableSaving", null);
- });
- }
-
- private void updateConfig1To2() {
- plugin.getServer().getScheduler().runTask(plugin, () -> {
- // Get the old config settings
- boolean notifySilentChest = plugin.getConfig().getBoolean("NotifySilentChest", true);
- boolean notifyAnyChest = plugin.getConfig().getBoolean("NotifyAnyChest", true);
- plugin.getConfig().set("ItemOpenInvItemID", null);
- plugin.getConfig().set("NotifySilentChest", null);
- plugin.getConfig().set("NotifyAnyChest", null);
- plugin.getConfig().set("config-version", 2);
- plugin.getConfig().set("notify.any-chest", notifyAnyChest);
- plugin.getConfig().set("notify.silent-chest", notifySilentChest);
- });
-
- updateToggles("AnyChest", "toggles.any-chest");
- updateToggles("SilentChest", "toggles.silent-chest");
- }
-
- private void updateToggles(final String sectionName, final String newSectionName) {
- ConfigurationSection section = plugin.getConfig().getConfigurationSection(sectionName);
- // Ensure section exists
- if (section == null) {
- return;
- }
-
- Set keys = section.getKeys(false);
-
- // Ensure section has content
- if (keys.isEmpty()) {
- return;
- }
-
- final Map toggles = new HashMap<>();
-
- for (String playerName : keys) {
- OfflinePlayer player = plugin.matchPlayer(playerName);
- if (player != null) {
- toggles.put(plugin.getPlayerID(player), section.getBoolean(playerName + ".toggle", false));
- }
- }
-
- plugin.getServer().getScheduler().runTask(plugin, () -> {
- // Wipe old ConfigurationSection
- plugin.getConfig().set(sectionName, null);
-
- // Prepare new ConfigurationSection
- ConfigurationSection newSection = plugin.getConfig().getConfigurationSection(newSectionName);
- if (newSection == null) {
- newSection = plugin.getConfig().createSection(newSectionName);
- }
- // Set new values
- for (Map.Entry entry : toggles.entrySet()) {
- newSection.set(entry.getKey(), entry.getValue());
- }
- });
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java b/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java
index fb245521..f5d77139 100644
--- a/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java
+++ b/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 lishid. All rights reserved.
+ * Copyright (C) 2011-2023 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,204 +16,294 @@
package com.lishid.openinv.util;
+import com.github.jikoo.planarwrappers.util.version.BukkitVersions;
+import com.github.jikoo.planarwrappers.util.version.Version;
+import com.lishid.openinv.internal.Accessor;
import com.lishid.openinv.internal.IAnySilentContainer;
-import com.lishid.openinv.internal.IPlayerDataManager;
import com.lishid.openinv.internal.ISpecialEnderChest;
+import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
+import com.lishid.openinv.internal.PlayerManager;
+import com.lishid.openinv.util.lang.LanguageManager;
+import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
-import org.bukkit.plugin.Plugin;
+import org.bukkit.inventory.InventoryView;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.logging.Logger;
public class InternalAccessor {
- private final Plugin plugin;
- private final String version;
- private boolean supported = false;
- private IPlayerDataManager playerDataManager;
- private IAnySilentContainer anySilentContainer;
-
- public InternalAccessor(final Plugin plugin) {
- this.plugin = plugin;
-
- String packageName = plugin.getServer().getClass().getPackage().getName();
- this.version = packageName.substring(packageName.lastIndexOf('.') + 1);
-
- try {
- Class.forName("com.lishid.openinv.internal." + this.version + ".SpecialPlayerInventory");
- Class.forName("com.lishid.openinv.internal." + this.version + ".SpecialEnderChest");
- this.playerDataManager = this.createObject(IPlayerDataManager.class, "PlayerDataManager");
- this.anySilentContainer = this.createObject(IAnySilentContainer.class, "AnySilentContainer");
- this.supported = InventoryAccess.isUsable();
- } catch (Exception ignored) {}
- }
-
- public String getReleasesLink() {
- switch (version) {
- case "1_4_5":
- case "1_4_6":
- case "v1_4_R1":
- case "v1_5_R2":
- case "v1_5_R3":
- case "v1_6_R1":
- case "v1_6_R2":
- case "v1_6_R3":
- case "v1_7_R1":
- case "v1_7_R2":
- case "v1_7_R3":
- case "v1_7_R4":
- case "v1_8_R1":
- case "v1_8_R2":
- case "v1_9_R1":
- case "v1_9_R2":
- case "v1_10_R1":
- case "v1_11_R1":
- case "v1_12_R1":
- return "https://github.com/lishid/OpenInv/releases/tag/4.0.0 (OpenInv-legacy)";
- case "v1_13_R1":
- return "https://github.com/lishid/OpenInv/releases/tag/4.0.0";
- case "v1_13_R2":
- return "https://github.com/lishid/OpenInv/releases/tag/4.0.7";
- case "v1_14_R1":
- return "https://github.com/lishid/OpenInv/releases/tag/4.1.1";
- case "v1_16_R1":
- return "https://github.com/lishid/OpenInv/releases/tag/4.1.4";
- case "v1_8_R3":
- case "v1_15_R1":
- case "v1_16_R2":
- return "https://github.com/lishid/OpenInv/releases/tag/4.1.5";
- case "v1_16_R3":
- default:
- return "https://github.com/lishid/OpenInv/releases";
- }
- }
-
- private T createObject(final Class extends T> assignableClass, final String className,
- final Object... params) throws ClassCastException, ClassNotFoundException,
- InstantiationException, IllegalAccessException, IllegalArgumentException,
- InvocationTargetException, NoSuchMethodException, SecurityException {
- // Fetch internal class if it exists.
- Class> internalClass = Class.forName("com.lishid.openinv.internal." + this.version + "." + className);
- if (!assignableClass.isAssignableFrom(internalClass)) {
- String message = String.format("Found class %s but cannot cast to %s!", internalClass.getName(), assignableClass.getName());
- this.plugin.getLogger().warning(message);
- throw new IllegalStateException(message);
- }
-
- // Quick return: no parameters, no need to fiddle about finding the correct constructor.
- if (params.length == 0) {
- return assignableClass.cast(internalClass.getConstructor().newInstance());
- }
-
- // Search constructors for one matching the given parameters
- nextConstructor: for (Constructor> constructor : internalClass.getConstructors()) {
- Class>[] requiredClasses = constructor.getParameterTypes();
- if (requiredClasses.length != params.length) {
- continue;
- }
- for (int i = 0; i < params.length; ++i) {
- if (!requiredClasses[i].isAssignableFrom(params[i].getClass())) {
- continue nextConstructor;
- }
- }
- return assignableClass.cast(constructor.newInstance(params));
- }
-
- StringBuilder builder = new StringBuilder("Found class ").append(internalClass.getName())
- .append(" but cannot find any matching constructors for [");
- for (Object object : params) {
- builder.append(object.getClass().getName()).append(", ");
- }
- builder.delete(builder.length() - 2, builder.length());
-
- String message = builder.append(']').toString();
- this.plugin.getLogger().warning(message);
-
- throw new IllegalArgumentException(message);
- }
-
- /**
- * Creates an instance of the IAnySilentContainer implementation for the current server version.
- *
- * @return the IAnySilentContainer
- * @throws IllegalStateException if server version is unsupported
- */
- public IAnySilentContainer getAnySilentContainer() {
- if (!this.supported) {
- throw new IllegalStateException(String.format("Unsupported server version %s!", this.version));
- }
- return this.anySilentContainer;
- }
-
- /**
- * Creates an instance of the IPlayerDataManager implementation for the current server version.
- *
- * @return the IPlayerDataManager
- * @throws IllegalStateException if server version is unsupported
- */
- public IPlayerDataManager getPlayerDataManager() {
- if (!this.supported) {
- throw new IllegalStateException(String.format("Unsupported server version %s!", this.version));
- }
- return this.playerDataManager;
- }
-
- /**
- * Gets the server implementation version. If not initialized, returns the string "null"
- * instead.
- *
- * @return the version, or "null"
- */
- public String getVersion() {
- return this.version != null ? this.version : "null";
- }
-
- /**
- * Checks if the server implementation is supported.
- *
- * @return true if initialized for a supported server version
- */
- public boolean isSupported() {
- return this.supported;
- }
-
- /**
- * Creates an instance of the ISpecialEnderChest implementation for the given Player, or
- * null if the current version is unsupported.
- *
- * @param player the Player
- * @param online true if the Player is online
- * @return the ISpecialEnderChest created
- * @throws InstantiationException if the ISpecialEnderChest could not be instantiated
- */
- public ISpecialEnderChest newSpecialEnderChest(final Player player, final boolean online) throws InstantiationException {
- if (!this.supported) {
- throw new IllegalStateException(String.format("Unsupported server version %s!", this.version));
- }
- try {
- return this.createObject(ISpecialEnderChest.class, "SpecialEnderChest", player, online);
- } catch (Exception e) {
- throw new InstantiationException(String.format("Unable to create a new ISpecialEnderChest: %s", e.getMessage()));
- }
- }
-
- /**
- * Creates an instance of the ISpecialPlayerInventory implementation for the given Player..
- *
- * @param player the Player
- * @param online true if the Player is online
- * @return the ISpecialPlayerInventory created
- * @throws InstantiationException if the ISpecialPlayerInventory could not be instantiated
- */
- public ISpecialPlayerInventory newSpecialPlayerInventory(final Player player, final boolean online) throws InstantiationException {
- if (!this.supported) {
- throw new IllegalStateException(String.format("Unsupported server version %s!", this.version));
- }
- try {
- return this.createObject(ISpecialPlayerInventory.class, "SpecialPlayerInventory", player, online);
- } catch (Exception e) {
- throw new InstantiationException(String.format("Unable to create a new ISpecialPlayerInventory: %s", e.getMessage()));
- }
+ private static final boolean PAPER;
+
+ static {
+ boolean paper = false;
+ try {
+ Class.forName("io.papermc.paper.configuration.GlobalConfiguration");
+ paper = true;
+ } catch (ClassNotFoundException ignored) {
+ // Expect remapped server.
+ }
+ PAPER = paper;
+ }
+
+ private @Nullable Accessor internal;
+
+ public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ try {
+ internal = getAccessor(logger, lang);
+
+ if (internal != null) {
+ InventoryAccess.setProvider(internal::get);
+ }
+ } catch (NoClassDefFoundError | Exception e) {
+ internal = null;
+ InventoryAccess.setProvider(null);
+ }
+ }
+
+ private @Nullable Accessor getAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
+ Version maxSupported = Version.of(1, 21, 11);
+ Version minSupported = Version.of(1, 21, 1);
+
+ // Ensure version is in supported range.
+ if (BukkitVersions.MINECRAFT.greaterThan(maxSupported) || BukkitVersions.MINECRAFT.lessThan(minSupported)) {
+ return null;
+ }
+
+ // Load Spigot accessor.
+ if (!PAPER) {
+ if (BukkitVersions.MINECRAFT.equals(maxSupported)) {
+ // Current Spigot, remapped internals are available.
+ return new com.lishid.openinv.internal.reobf.InternalAccessor(logger, lang);
+ } else {
+ // Older Spigot; unsupported.
+ return null;
+ }
+ }
+
+ // Paper or a Paper fork, can use Mojang-mapped internals.
+ if (BukkitVersions.MINECRAFT.equals(maxSupported)) { // 1.21.11
+ return new com.lishid.openinv.internal.common.InternalAccessor(logger, lang);
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10))
+ && BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 9))) { // 1.21.9, 1.21.10
+ return new com.lishid.openinv.internal.paper1_21_10.InternalAccessor(logger, lang);
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8))
+ && BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 6))) { // 1.21.6, 1.21.7, 1.21.8
+ return new com.lishid.openinv.internal.paper1_21_8.InternalAccessor(logger, lang);
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 5))) { // 1.21.5
+ return new com.lishid.openinv.internal.paper1_21_5.InternalAccessor(logger, lang);
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 4))) { // 1.21.4
+ return new com.lishid.openinv.internal.paper1_21_4.InternalAccessor(logger, lang);
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 21, 2))) {
+ // 1.21.1-1.21.2 placeholder format
+ return new com.lishid.openinv.internal.paper1_21_1.InternalAccessor(logger, lang);
+ }
+
+ // 1.21.2, 1.21.3
+ return new com.lishid.openinv.internal.paper1_21_3.InternalAccessor(logger, lang);
+ }
+
+ public String getReleasesLink() {
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 4, 4))) { // Good luck.
+ return "https://dev.bukkit.org/projects/openinv/files?&sort=datecreated";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 8, 8))) { // 1.8.8
+ return "https://github.com/lishid/OpenInv/releases/tag/4.1.5";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 13))) { // 1.4.4+ had versioned packages.
+ return "https://github.com/lishid/OpenInv/releases/tag/4.0.0 (OpenInv-legacy)";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 13))) { // 1.13
+ return "https://github.com/lishid/OpenInv/releases/tag/4.0.0";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 14))) { // 1.13.1, 1.13.2
+ return "https://github.com/lishid/OpenInv/releases/tag/4.0.7";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 14))) { // 1.14 to 1.14.1 had no revision bump.
+ return "https://github.com/lishid/OpenInv/releases/tag/4.0.0";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 14, 1))) { // 1.14.1 to 1.14.2 had no revision bump.
+ return "https://github.com/lishid/OpenInv/releases/tag/4.0.1";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 15))) { // 1.14.2
+ return "https://github.com/lishid/OpenInv/releases/tag/4.1.1";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 15, 1))) { // 1.15, 1.15.1
+ return "https://github.com/lishid/OpenInv/releases/tag/4.1.5";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 16))) { // 1.15.2
+ return "https://github.com/Jikoo/OpenInv/commit/502f661be39ee85d300851dd571f3da226f12345 (never released)";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 16, 1))) { // 1.16, 1.16.1
+ return "https://github.com/lishid/OpenInv/releases/tag/4.1.4";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 16, 3))) { // 1.16.2, 1.16.3
+ return "https://github.com/lishid/OpenInv/releases/tag/4.1.5";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 17))) { // 1.16.4, 1.16.5
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.1.8";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 18, 1))) { // 1.17, 1.18, 1.18.1
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.1.10";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 19))) { // 1.18.2
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.3.0";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 19))) { // 1.19
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.2.0";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 19, 1))) { // 1.19.1
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.2.2";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 19, 3))) { // 1.19.2, 1.19.3
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.3.0";
+ }
+ if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 20))) { // 1.19.4
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.3";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 20, 1))) { // 1.20, 1.20.1
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.1";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 20, 3))) { // 1.20.2, 1.20.3
+ return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.3";
+ }
+ if (BukkitVersions.MINECRAFT.equals(Version.of(1, 20, 5))) { // 1.20.5
+ return "Unsupported; upgrade to 1.20.6: https://github.com/Jikoo/OpenInv/releases/tag/5.1.2";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21))) { // 1.20.4, 1.20.6, 1.21
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.2";
+ }
+ if (!PAPER) {
+ return getSpigotReleaseLink();
+ }
+ // Paper 1.21.1+
+ return "https://github.com/Jikoo/OpenInv/releases";
+ }
+
+ private String getSpigotReleaseLink() {
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 2))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.3";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 3))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.6";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 4))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.9";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 5))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.11";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 6))) {
+ return "Unsupported; upgrade to 1.21.7: https://github.com/Jikoo/OpenInv/releases";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.13";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 9))) {
+ return "Unsupported; upgrade to 1.21.10: https://github.com/Jikoo/OpenInv/releases/tag/5.1.15";
+ }
+ if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10))) {
+ return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.15";
+ }
+
+ return "https://github.com/Jikoo/OpenInv/releases";
+ }
+
+ /**
+ * Reload internal features.
+ */
+ public void reload(ConfigurationSection config) {
+ if (internal != null) {
+ internal.reload(config);
+ }
+ }
+
+ /**
+ * Gets the server implementation version.
+ *
+ * @return the version
+ */
+ public @NotNull String getVersion() {
+ return BukkitVersions.MINECRAFT.toString();
+ }
+
+ /**
+ * Checks if the server implementation is supported.
+ *
+ * @return true if initialized for a supported server version
+ */
+ public boolean isSupported() {
+ return internal != null;
+ }
+
+ /**
+ * Get the instance of the IAnySilentContainer implementation for the current server version.
+ *
+ * @return the IAnySilentContainer
+ * @throws IllegalStateException if server version is unsupported
+ */
+ public @NotNull IAnySilentContainer getAnySilentContainer() {
+ if (internal == null) {
+ throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
+ }
+ return internal.getAnySilentContainer();
+ }
+
+ public @Nullable InventoryView openInventory(
+ @NotNull Player player,
+ @NotNull ISpecialInventory inventory,
+ boolean viewOnly
+ ) {
+ if (internal == null) {
+ throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
+ }
+ return internal.getPlayerManager().openInventory(player, inventory, viewOnly);
+ }
+
+ /**
+ * Get the instance of the IPlayerDataManager implementation for the current server version.
+ *
+ * @return the IPlayerDataManager
+ * @throws IllegalStateException if server version is unsupported
+ */
+ @NotNull PlayerManager getPlayerDataManager() {
+ if (internal == null) {
+ throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
+ }
+ return internal.getPlayerManager();
+ }
+
+ /**
+ * Creates an instance of the ISpecialEnderChest implementation for the given Player.
+ *
+ * @param player the Player
+ * @return the ISpecialEnderChest created
+ */
+ ISpecialEnderChest createEnderChest(final Player player) {
+ if (internal == null) {
+ throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
+ }
+ return internal.createEnderChest(player);
+ }
+
+ /**
+ * Creates an instance of the ISpecialPlayerInventory implementation for the given Player.
+ *
+ * @param player the Player
+ * @return the ISpecialPlayerInventory created
+ */
+ ISpecialPlayerInventory createInventory(final Player player) {
+ if (internal == null) {
+ throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
}
+ return internal.createPlayerInventory(player);
+ }
}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java b/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java
new file mode 100644
index 00000000..d6460197
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java
@@ -0,0 +1,277 @@
+package com.lishid.openinv.util;
+
+import com.google.errorprone.annotations.Keep;
+import com.lishid.openinv.OpenInv;
+import com.lishid.openinv.event.OpenEvents;
+import com.lishid.openinv.internal.ISpecialEnderChest;
+import com.lishid.openinv.internal.ISpecialInventory;
+import com.lishid.openinv.internal.ISpecialPlayerInventory;
+import com.lishid.openinv.util.config.Config;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.event.player.PlayerChangedWorldEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.logging.Level;
+import java.util.stream.Stream;
+
+/**
+ * A manager for special inventories. Delegates creation and tracks copies in use.
+ */
+public class InventoryManager implements Listener {
+
+ private final Map inventories = new ConcurrentHashMap<>();
+ private final Map enderChests = new ConcurrentHashMap<>();
+ private final Set expectedCloses = new HashSet<>();
+ private final @NotNull OpenInv plugin;
+ private final @NotNull Config config;
+ private final @NotNull InternalAccessor accessor;
+
+ public InventoryManager(@NotNull OpenInv plugin, @NotNull Config config, @NotNull InternalAccessor accessor) {
+ this.plugin = plugin;
+ this.config = config;
+ this.accessor = accessor;
+ }
+
+ public void evictAll() {
+ Stream.concat(inventories.values().stream(), enderChests.values().stream())
+ .map(inventory -> {
+ // Rather than iterate twice, evict all viewers during remapping.
+ for (HumanEntity viewer : List.copyOf(inventory.getBukkitInventory().getViewers())) {
+ expectedCloses.add(viewer.getUniqueId());
+ viewer.closeInventory();
+ }
+ // If saving is prevented, return a null value for the player to save.
+ if (config.isSaveDisabled() || OpenEvents.saveCancelled(inventory)) {
+ return null;
+ }
+ if (inventory.getPlayer() instanceof Player player) {
+ return player;
+ }
+ return null;
+ })
+ .filter(Objects::nonNull)
+ .distinct()
+ .forEach(player -> {
+ if (!player.isOnline()) {
+ accessor.getPlayerDataManager().inject(player).saveData();
+ }
+ });
+ inventories.clear();
+ enderChests.clear();
+ expectedCloses.clear();
+ }
+
+ public @NotNull ISpecialPlayerInventory getInventory(@NotNull Player player) {
+ return inventories.computeIfAbsent(player.getUniqueId(), uuid -> accessor.createInventory(player));
+ }
+
+ public @NotNull ISpecialEnderChest getEnderChest(@NotNull Player player) {
+ return enderChests.computeIfAbsent(player.getUniqueId(), uuid -> accessor.createEnderChest(player));
+ }
+
+ public @Nullable Player getLoadedPlayer(@NotNull UUID uuid) {
+ ISpecialInventory inUse = inventories.get(uuid);
+ if (inUse != null) {
+ return (Player) inUse.getPlayer();
+ }
+ inUse = enderChests.get(uuid);
+ if (inUse != null) {
+ return (Player) inUse.getPlayer();
+ }
+ return null;
+ }
+
+ public void unload(@NotNull UUID uuid) {
+ inventories.computeIfPresent(uuid, this::remove);
+ enderChests.computeIfPresent(uuid, this::remove);
+ }
+
+ public void save(@NotNull UUID uuid) {
+ consumeLoaded(uuid, inventory -> {});
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.LOWEST)
+ private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
+ consumeLoaded(
+ event.getPlayer().getUniqueId(),
+ inventory -> {
+ inventory.setPlayerOnline(event.getPlayer());
+ checkViewerAccess(inventory, true);
+ }
+ );
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.MONITOR)
+ private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
+ consumeLoaded(
+ event.getPlayer().getUniqueId(),
+ inventory -> checkViewerAccess(inventory, false)
+ );
+ }
+
+ @Keep
+ @EventHandler
+ private void onWorldChanged(@NotNull PlayerChangedWorldEvent event) {
+ Player player = event.getPlayer();
+ consumeLoaded(player.getUniqueId(), inventory -> checkViewerAccess(inventory, player.isOnline()));
+ }
+
+ @Keep
+ @EventHandler
+ private void onInventoryClose(@NotNull InventoryCloseEvent event) {
+ ISpecialInventory inventory = InventoryAccess.getInventory(event.getInventory());
+
+ // If this is not an ISpecialInventory or the inventory was closed elsewhere internally, don't handle.
+ if (inventory == null || expectedCloses.remove(event.getPlayer().getUniqueId())) {
+ return;
+ }
+
+ // Fetch the active ISpecialInventory of this type.
+ Map map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests;
+ UUID key = inventory.getPlayer().getUniqueId();
+ ISpecialInventory loaded = map.get(key);
+
+ // If there is no loaded inventory, it has already been removed and saved.
+ if (loaded == null) {
+ return;
+ }
+
+ // This should only be possible if a plugin is going to extreme lengths to mess with our inventories.
+ if (loaded != inventory) {
+ // Immediately remove affected inventory, then dump all viewers. We don't want to risk duplication bugs.
+ map.remove(key);
+ remove(key, loaded);
+ remove(key, inventory);
+ // The loaded one is "correct" as far as we're concerned, so save that.
+ save(loaded);
+ }
+
+ // Schedule task to check in use status later this tick. Closing user is still in viewer list.
+ plugin.getScheduler().runTask(() -> {
+ if (loaded.isInUse()) {
+ return;
+ }
+
+ // Re-fetch from map to reduce odds of a duplicate save.
+ ISpecialInventory current = map.remove(key);
+
+ if (current != null) {
+ save(current);
+ }
+ });
+ }
+
+ @Keep
+ @EventHandler(priority = EventPriority.HIGHEST)
+ private void onInventoryOpen(@NotNull InventoryOpenEvent event) {
+ ISpecialInventory inventory = InventoryAccess.getInventory(event.getInventory());
+ if (inventory == null) {
+ return;
+ }
+
+ Map map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests;
+ UUID key = inventory.getPlayer().getUniqueId();
+ ISpecialInventory loaded = map.get(key);
+
+ if (!inventory.equals(loaded)) {
+ event.setCancelled(true);
+ plugin.getLogger().log(
+ Level.WARNING,
+ "Prevented a plugin from opening an untracked ISpecialInventory!",
+ new Throwable("Untracked ISpecialInventory")
+ );
+ }
+ }
+
+ private void checkViewerAccess(@NotNull T inventory, boolean online) {
+
+ Player owner = (Player) inventory.getPlayer();
+ Permissions connectedState = online ? Permissions.ACCESS_ONLINE : Permissions.ACCESS_OFFLINE;
+ boolean alwaysDenied = !online && config.isOfflineDisabled();
+
+ // Copy viewers so we don't modify the list we're iterating over when closing inventories.
+ List viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers());
+
+ for (HumanEntity viewer : viewers) {
+ if (alwaysDenied
+ || !connectedState.hasPermission(viewer)
+ || (!Objects.equals(owner.getWorld(), viewer.getWorld()) && !Permissions.ACCESS_CROSSWORLD.hasPermission(viewer))) {
+ expectedCloses.add(viewer.getUniqueId());
+ viewer.closeInventory();
+ }
+ }
+ }
+
+ private void consumeLoaded(@NotNull UUID key, @NotNull Consumer<@NotNull ISpecialInventory> consumer) {
+ boolean saved = consumeLoaded(inventories, key, false, consumer);
+ consumeLoaded(enderChests, key, saved, consumer);
+ }
+
+ private boolean consumeLoaded(
+ @NotNull Map map,
+ @NotNull UUID key,
+ boolean saved,
+ @NotNull Consumer<@NotNull ISpecialInventory> consumer
+ ) {
+ T inventory = map.get(key);
+
+ if (inventory == null) {
+ return saved;
+ }
+
+ consumer.accept(inventory);
+ if (!inventory.isInUse()) {
+ map.remove(key);
+
+ if (!saved) {
+ save(inventory);
+ return true;
+ }
+ }
+
+ return saved;
+ }
+
+ private void save(@NotNull ISpecialInventory inventory) {
+ if (config.isSaveDisabled()) {
+ return;
+ }
+
+ Player player = (Player) inventory.getPlayer();
+
+ if (!player.isOnline() && !OpenEvents.saveCancelled(inventory)) {
+ accessor.getPlayerDataManager().inject(player).saveData();
+ }
+ }
+
+ @Contract("_, _ -> null")
+ private @Nullable T remove(@NotNull UUID key, @NotNull T inventory) {
+ for (HumanEntity viewer : List.copyOf(inventory.getBukkitInventory().getViewers())) {
+ expectedCloses.add(viewer.getUniqueId());
+ viewer.closeInventory();
+ }
+ return null;
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java b/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java
deleted file mode 100644
index 8eadcb9f..00000000
--- a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2011-2020 Jikoo. 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.lishid.openinv.OpenInv;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.logging.Level;
-import org.bukkit.ChatColor;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * A simple language manager supporting both custom and bundled languages.
- *
- * @author Jikoo
- */
-public class LanguageManager {
-
- private final OpenInv plugin;
- private final String defaultLocale;
- private final Map locales;
-
- public LanguageManager(@NotNull OpenInv plugin, @NotNull String defaultLocale) {
- this.plugin = plugin;
- this.defaultLocale = defaultLocale;
- this.locales = new HashMap<>();
- getOrLoadLocale(defaultLocale);
- }
-
- private YamlConfiguration getOrLoadLocale(@NotNull String locale) {
- YamlConfiguration loaded = locales.get(locale);
- if (loaded != null) {
- return loaded;
- }
-
- InputStream resourceStream = plugin.getResource(locale + ".yml");
- YamlConfiguration localeConfigDefaults;
- if (resourceStream == null) {
- localeConfigDefaults = new YamlConfiguration();
- } else {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
- localeConfigDefaults = YamlConfiguration.loadConfiguration(reader);
- } catch (IOException e) {
- plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to load resource " + locale + ".yml", e);
- localeConfigDefaults = new YamlConfiguration();
- }
- }
-
- File file = new File(plugin.getDataFolder(), locale + ".yml");
- YamlConfiguration localeConfig;
-
- if (!file.exists()) {
- localeConfig = localeConfigDefaults;
- try {
- localeConfigDefaults.save(file);
- } catch (IOException e) {
- plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e);
- }
- } else {
- localeConfig = YamlConfiguration.loadConfiguration(file);
-
- // Add new language keys
- List newKeys = new ArrayList<>();
- for (String key : localeConfigDefaults.getKeys(true)) {
- if (localeConfigDefaults.isConfigurationSection(key)) {
- continue;
- }
-
- if (localeConfig.isSet(key)) {
- continue;
- }
-
- localeConfig.set(key, localeConfigDefaults.get(key));
- newKeys.add(key);
- }
-
- if (!newKeys.isEmpty()) {
- plugin.getLogger().info("[LanguageManager] Added new language keys: " + String.join(", ", newKeys));
- try {
- localeConfig.save(file);
- } catch (IOException e) {
- plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e);
- }
- }
- }
-
- if (!locale.equals(defaultLocale)) {
- localeConfigDefaults = locales.get(defaultLocale);
-
- // Check for missing keys
- List newKeys = new ArrayList<>();
- for (String key : localeConfigDefaults.getKeys(true)) {
- if (localeConfigDefaults.isConfigurationSection(key)) {
- continue;
- }
-
- if (localeConfig.isSet(key)) {
- continue;
- }
-
- newKeys.add(key);
- }
-
- if (!newKeys.isEmpty()) {
- plugin.getLogger().info("[LanguageManager] Missing translations from " + locale + ".yml: " + String.join(", ", newKeys));
- }
-
- // Fall through to default locale
- localeConfig.setDefaults(localeConfigDefaults);
- }
-
- locales.put(locale, localeConfig);
- return localeConfig;
- }
-
- @Nullable
- public String getValue(@NotNull String key, @Nullable String locale) {
- String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase()).getString(key);
- if (value == null || value.isEmpty()) {
- return null;
- }
-
- value = ChatColor.translateAlternateColorCodes('&', value);
-
- return value;
- }
-
- @Nullable
- public String getValue(@NotNull String key, @Nullable String locale, @NotNull String... replacements) {
- if (replacements.length % 2 != 0) {
- plugin.getLogger().log(Level.WARNING, "[LanguageManager] Replacement data is uneven", new Exception());
- }
-
- String value = getValue(key, locale);
-
- if (value == null) {
- return null;
- }
-
- for (int i = 0; i < replacements.length; i += 2) {
- value = value.replace(replacements[i], replacements[i + 1]);
- }
-
- return value;
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java b/plugin/src/main/java/com/lishid/openinv/util/Permissions.java
deleted file mode 100644
index 859ba139..00000000
--- a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2011-2020 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 org.bukkit.permissions.Permissible;
-
-public enum Permissions {
-
- OPENINV("openinv"),
- OVERRIDE("override"),
- EXEMPT("exempt"),
- CROSSWORLD("crossworld"),
- SILENT("silent"),
- SILENT_DEFAULT("silent.default", true),
- ANYCHEST("anychest"),
- ANY_DEFAULT("any.default", true),
- ENDERCHEST("openender"),
- ENDERCHEST_ALL("openenderall"),
- SEARCH("search"),
- EDITINV("editinv"),
- EDITENDER("editender"),
- OPENSELF("openself"),
- OPENONLINE("openonline"),
- OPENOFFLINE("openoffline"),
- SPECTATE("spectate");
-
- private final String permission;
- private final boolean uninheritable;
-
- Permissions(String permission) {
- this(permission, false);
- }
-
- Permissions(String permission, boolean uninheritable) {
- this.permission = "OpenInv." + permission;
- this.uninheritable = uninheritable;
- }
-
- public boolean hasPermission(Permissible permissible) {
-
- boolean hasPermission = permissible.hasPermission(permission);
- if (uninheritable || hasPermission || permissible.isPermissionSet(permission)) {
- return hasPermission;
- }
-
- StringBuilder permissionDestroyer = new StringBuilder(permission);
- for (int lastPeriod = permissionDestroyer.lastIndexOf("."); lastPeriod > 0;
- lastPeriod = permissionDestroyer.lastIndexOf(".")) {
- permissionDestroyer.delete(lastPeriod + 1, permissionDestroyer.length()).append('*');
-
- hasPermission = permissible.hasPermission(permissionDestroyer.toString());
- if (hasPermission || permissible.isPermissionSet(permissionDestroyer.toString())) {
- return hasPermission;
- }
-
- permissionDestroyer.delete(lastPeriod, permissionDestroyer.length());
-
- }
-
- return permissible.hasPermission("*");
-
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java b/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java
new file mode 100644
index 00000000..0930c589
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java
@@ -0,0 +1,236 @@
+package com.lishid.openinv.util;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.errorprone.annotations.Keep;
+import com.lishid.openinv.OpenInv;
+import com.lishid.openinv.util.config.Config;
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.profile.PlayerProfile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A utility for looking up and loading players.
+ */
+public class PlayerLoader implements Listener {
+
+ private final @NotNull OpenInv plugin;
+ private final @NotNull Config config;
+ private final @NotNull InventoryManager inventoryManager;
+ private final @NotNull InternalAccessor internalAccessor;
+ private final @NotNull Logger logger;
+ private final @NotNull Cache lookupCache;
+
+ public PlayerLoader(
+ @NotNull OpenInv plugin,
+ @NotNull Config config,
+ @NotNull InventoryManager inventoryManager,
+ @NotNull InternalAccessor internalAccessor,
+ @NotNull Logger logger
+ ) {
+ this.plugin = plugin;
+ this.config = config;
+ this.inventoryManager = inventoryManager;
+ this.internalAccessor = internalAccessor;
+ this.logger = logger;
+ this.lookupCache = CacheBuilder.newBuilder().maximumSize(20).build();
+ }
+
+ /**
+ * Load a {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for
+ * the server is not loaded, this will return {@code null}.
+ *
+ * @param offline the {@code OfflinePlayer} to load a {@code Player} for
+ * @return the loaded {@code Player}
+ * @throws IllegalStateException if the server version is unsupported
+ */
+ public @Nullable Player load(@NotNull OfflinePlayer offline) {
+ UUID key = offline.getUniqueId();
+
+ Player player = offline.getPlayer();
+ if (player != null) {
+ return player;
+ }
+
+ player = inventoryManager.getLoadedPlayer(key);
+ if (player != null) {
+ return player;
+ }
+
+ if (config.isOfflineDisabled() || !internalAccessor.isSupported()) {
+ return null;
+ }
+
+ if (Bukkit.isPrimaryThread()) {
+ return internalAccessor.getPlayerDataManager().loadPlayer(offline);
+ }
+
+ CompletableFuture future = new CompletableFuture<>();
+ plugin.getScheduler().runTask(() -> future.complete(internalAccessor.getPlayerDataManager().loadPlayer(offline)));
+
+ try {
+ player = future.get();
+ } catch (InterruptedException | ExecutionException e) {
+ logger.log(Level.WARNING, e.getMessage(), e);
+ return null;
+ }
+
+ return player;
+ }
+
+ public @Nullable OfflinePlayer matchExact(@NotNull String name) {
+ // Warn if called on the main thread - if we resort to searching offline players, this may take several seconds.
+ if (Bukkit.getServer().isPrimaryThread()) {
+ logger.warning("Call to PlayerSearchCache#matchPlayer made on the main thread!");
+ logger.warning("This can cause the server to hang, potentially severely.");
+ logger.log(Level.WARNING, "Current stack trace", new Throwable("Current stack trace"));
+ }
+
+ OfflinePlayer player;
+
+ try {
+ UUID uuid = UUID.fromString(name);
+ player = Bukkit.getOfflinePlayer(uuid);
+ // Ensure player is an existing player.
+ if (player.hasPlayedBefore() || player.isOnline()) {
+ return player;
+ }
+ // Return null otherwise.
+ return null;
+ } catch (IllegalArgumentException ignored) {
+ // Not a UUID
+ }
+
+ // Exact online match first.
+ player = Bukkit.getServer().getPlayerExact(name);
+
+ if (player != null) {
+ return player;
+ }
+
+ // Cached offline match.
+ PlayerProfile cachedResult = lookupCache.getIfPresent(name);
+ if (cachedResult != null && cachedResult.getUniqueId() != null) {
+ player = Bukkit.getOfflinePlayer(cachedResult.getUniqueId());
+ // Ensure player is an existing player.
+ if (player.hasPlayedBefore() || player.isOnline()) {
+ return player;
+ }
+ // Return null otherwise.
+ return null;
+ }
+
+ // Exact offline match second - ensure offline access works when matchable users are online.
+ player = Bukkit.getServer().getOfflinePlayer(name);
+
+ if (player.hasPlayedBefore()) {
+ lookupCache.put(name, player.getPlayerProfile());
+ return player;
+ }
+
+ return null;
+ }
+
+ public @Nullable OfflinePlayer match(@NotNull String name) {
+ OfflinePlayer player = this.matchExact(name);
+
+ if (player != null) {
+ return player;
+ }
+
+ // Inexact online match.
+ player = Bukkit.getServer().getPlayer(name);
+
+ if (player != null) {
+ return player;
+ }
+
+ // Finally, inexact offline match.
+ float bestMatch = 0;
+ for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) {
+ if (offline.getName() == null) {
+ // Loaded by UUID only, name has never been looked up.
+ continue;
+ }
+
+ float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName());
+
+ if (currentMatch == 1.0F) {
+ return offline;
+ }
+
+ if (currentMatch > bestMatch) {
+ bestMatch = currentMatch;
+ player = offline;
+ }
+ }
+
+ if (player != null) {
+ // If a match was found, store it.
+ lookupCache.put(name, player.getPlayerProfile());
+ return player;
+ }
+
+ // No players have ever joined the server.
+ return null;
+ }
+
+ @Keep
+ @EventHandler
+ private void updateMatches(@NotNull PlayerJoinEvent event) {
+ // If player is not new, any cached values are valid.
+ if (event.getPlayer().hasPlayedBefore()) {
+ return;
+ }
+
+ // New player may have a name that already points to someone else in lookup cache.
+ String name = event.getPlayer().getName();
+ lookupCache.invalidate(name);
+
+ // If the cache is empty, nothing to do. Don't hit scheduler.
+ if (lookupCache.size() == 0) {
+ return;
+ }
+
+ plugin.getScheduler().runTaskLaterAsynchronously(
+ () -> {
+ Iterator> iterator = lookupCache.asMap().entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ String oldMatch = entry.getValue().getName();
+
+ // Shouldn't be possible - all profiles should be complete.
+ if (oldMatch == null) {
+ iterator.remove();
+ continue;
+ }
+
+ String lookup = entry.getKey();
+ float oldMatchScore = StringMetric.compareJaroWinkler(lookup, oldMatch);
+ float newMatchScore = StringMetric.compareJaroWinkler(lookup, name);
+
+ // If new match exceeds old match, delete old match.
+ if (newMatchScore > oldMatchScore) {
+ iterator.remove();
+ }
+ }
+ },
+ 7L
+ );
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java b/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java
new file mode 100644
index 00000000..e62ad300
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java
@@ -0,0 +1,66 @@
+package com.lishid.openinv.util;
+
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import org.bukkit.inventory.meta.BundleMeta;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Predicate;
+
+public final class SearchHelper {
+
+ public static boolean findMatch(@NotNull Inventory inventory, @NotNull Predicate<@NotNull ItemStack> predicate) {
+ for (ItemStack content : inventory.getContents()) {
+ if (findMatch(content, predicate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean findMatch(@Nullable ItemStack itemStack, @NotNull Predicate<@NotNull ItemStack> predicate) {
+ if (itemStack == null || itemStack.getType().isAir()) {
+ return false;
+ }
+
+ // If the item is the search target, done.
+ if (predicate.test(itemStack)) {
+ return true;
+ }
+
+ // If the item doesn't have meta, it cannot contain items.
+ if (!itemStack.hasItemMeta()) {
+ return false;
+ }
+
+ ItemMeta meta = itemStack.getItemMeta();
+
+ // Container meta with items (primarily shulkers).
+ if (meta instanceof BlockStateMeta stateMeta) {
+ if (!stateMeta.hasBlockState() || !(stateMeta.getBlockState() instanceof InventoryHolder holder)) {
+ return false;
+ }
+ Inventory inventory = holder.getInventory();
+ return findMatch(inventory, predicate);
+ }
+
+ // Bundle meta.
+ if (meta instanceof BundleMeta bundleMeta) {
+ for (ItemStack subStack : bundleMeta.getItems()) {
+ if (findMatch(subStack, predicate)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private SearchHelper() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java b/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java
new file mode 100644
index 00000000..8feaca81
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java
@@ -0,0 +1,164 @@
+/*
+ * This file is an amalgamation of code from the Simmetrics authors.
+ * The originals may be found here:
+ * https://github.com/Simmetrics/simmetrics/blob/master/simmetrics-core/src/main/java/org/simmetrics/metrics/JaroWinkler.java
+ * https://github.com/Simmetrics/simmetrics/blob/master/simmetrics-core/src/main/java/org/simmetrics/metrics/Jaro.java
+ *
+ * Copyright (C) 2014 - 2016 Simmetrics Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.lishid.openinv.util;
+
+public class StringMetric {
+
+ public static float compareJaroWinkler(String a, String b) {
+ final float jaroScore = compareJaro(a, b);
+
+ if (jaroScore < (float) 0.7) {
+ return jaroScore;
+ }
+
+ String prefix = commonPrefix(a, b);
+ int prefixLength = Math.min(prefix.codePointCount(0, prefix.length()), 4);
+
+ return jaroScore + (prefixLength * (float) 0.1 * (1.0f - jaroScore));
+
+ }
+
+ private static float compareJaro(String a, String b) {
+ if (a.isEmpty() && b.isEmpty()) {
+ return 1.0f;
+ }
+
+ if (a.isEmpty() || b.isEmpty()) {
+ return 0.0f;
+ }
+
+ final int[] charsA = a.codePoints().toArray();
+ final int[] charsB = b.codePoints().toArray();
+
+ // Intentional integer division to round down.
+ final int halfLength = Math.max(0, Math.max(charsA.length, charsB.length) / 2 - 1);
+
+ final int[] commonA = getCommonCodePoints(charsA, charsB, halfLength);
+ final int[] commonB = getCommonCodePoints(charsB, charsA, halfLength);
+
+ // commonA and commonB will always contain the same multi-set of
+ // characters. Because getCommonCharacters has been optimized, commonA
+ // and commonB are -1-padded. So in this loop we count transposition
+ // and use commonCharacters to determine the length of the multi-set.
+ float transpositions = 0;
+ int commonCharacters = 0;
+ for (
+ int length = commonA.length;
+ commonCharacters < length && commonA[commonCharacters] > -1;
+ commonCharacters++
+ ) {
+ if (commonA[commonCharacters] != commonB[commonCharacters]) {
+ transpositions++;
+ }
+ }
+
+ if (commonCharacters == 0) {
+ return 0.0f;
+ }
+
+ float aCommonRatio = commonCharacters / (float) charsA.length;
+ float bCommonRatio = commonCharacters / (float) charsB.length;
+ float transpositionRatio = (commonCharacters - transpositions / 2.0f) / commonCharacters;
+
+ return (aCommonRatio + bCommonRatio + transpositionRatio) / 3.0f;
+ }
+
+ /*
+ * Returns an array of code points from a within b. A character in b is
+ * counted as common when it is within separation distance from the position
+ * in a.
+ */
+ private static int[] getCommonCodePoints(final int[] charsA, final int[] charsB, final int separation) {
+ final int[] common = new int[Math.min(charsA.length, charsB.length)];
+ final boolean[] matched = new boolean[charsB.length];
+
+ // Iterate of string a and find all characters that occur in b within
+ // the separation distance. Mark any matches found to avoid
+ // duplicate matchings.
+ int commonIndex = 0;
+ for (int i = 0, length = charsA.length; i < length; i++) {
+ final int character = charsA[i];
+ final int index = indexOf(
+ character,
+ charsB,
+ i - separation,
+ i + separation + 1,
+ matched
+ );
+ if (index > -1) {
+ common[commonIndex++] = character;
+ matched[index] = true;
+ }
+ }
+
+ if (commonIndex < common.length) {
+ common[commonIndex] = -1;
+ }
+
+ // Both invocations will yield the same multi-set terminated by -1, so
+ // they can be compared for transposition without making a copy.
+ return common;
+ }
+
+ /*
+ * Search for code point in buffer starting at fromIndex to toIndex - 1.
+ *
+ * Returns -1 when not found.
+ */
+ private static int indexOf(int character, int[] buffer, int fromIndex, int toIndex, boolean[] matched) {
+
+ // compare char with range of characters to either side
+ for (int j = Math.max(0, fromIndex), length = Math.min(toIndex, buffer.length); j < length; j++) {
+ // check if found
+ if (buffer[j] == character && !matched[j]) {
+ return j;
+ }
+ }
+
+ return -1;
+ }
+
+ private static String commonPrefix(CharSequence a, CharSequence b) {
+ int maxPrefixLength = Math.min(a.length(), b.length());
+
+ int p;
+
+ p = 0;
+ while (p < maxPrefixLength && a.charAt(p) == b.charAt(p)) {
+ ++p;
+ }
+
+ if (validSurrogatePairAt(a, p - 1) || validSurrogatePairAt(b, p - 1)) {
+ --p;
+ }
+
+ return a.subSequence(0, p).toString();
+ }
+
+ private static boolean validSurrogatePairAt(CharSequence string, int index) {
+ return index >= 0 && index <= string.length() - 2 && Character.isHighSurrogate(string.charAt(index)) && Character.isLowSurrogate(string.charAt(index + 1));
+ }
+
+ private StringMetric() {
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java b/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java
index 77538f95..f55ae192 100644
--- a/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java
+++ b/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011-2020 lishid. All rights reserved.
+ * 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
@@ -16,132 +16,151 @@
package com.lishid.openinv.util;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.util.StringUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.Unmodifiable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
-import org.bukkit.Bukkit;
-import org.bukkit.command.CommandSender;
-import org.bukkit.entity.Player;
-import org.bukkit.util.StringUtil;
/**
* Utility class for common tab completions.
*/
-public class TabCompleter {
-
- /**
- * Offer tab completions for whole numbers.
- *
- * @param argument the argument to complete
- * @return integer options
- */
- public static List completeInteger(String argument) {
- // Ensure existing argument is actually a number
- if (!argument.isEmpty()) {
- try {
- Integer.parseInt(argument);
- } catch (NumberFormatException e) {
- return Collections.emptyList();
- }
- }
-
- List completions = new ArrayList<>(10);
- for (int i = 0; i < 10; ++i) {
- completions.add(argument + i);
- }
-
- return completions;
+public final class TabCompleter {
+
+ /**
+ * Offer tab completions for whole numbers.
+ *
+ * @param argument the argument to complete
+ * @return integer options
+ */
+ public static @NotNull @Unmodifiable List completeInteger(@NotNull String argument) {
+ // Ensure existing argument is actually a number
+ if (!argument.isEmpty()) {
+ try {
+ Integer.parseInt(argument);
+ } catch (NumberFormatException e) {
+ return List.of();
+ }
}
- /**
- * Offer tab completions for a given Enum.
- *
- * @param argument the argument to complete
- * @param enumClazz the Enum to complete for
- * @return the matching Enum values
- */
- public static List completeEnum(String argument, Class extends Enum>> enumClazz) {
- argument = argument.toLowerCase(Locale.ENGLISH);
- List completions = new ArrayList<>();
-
- for (Enum> enumConstant : enumClazz.getEnumConstants()) {
- String name = enumConstant.name().toLowerCase();
- if (name.startsWith(argument)) {
- completions.add(name);
- }
- }
-
- return completions;
+ List completions = new ArrayList<>(10);
+ for (int i = 0; i < 10; ++i) {
+ completions.add(argument + i);
}
- /**
- * Offer tab completions for a given array of Strings.
- *
- * @param argument the argument to complete
- * @param options the Strings which may be completed
- * @return the matching Strings
- */
- public static List completeString(String argument, String[] options) {
- argument = argument.toLowerCase(Locale.ENGLISH);
- List completions = new ArrayList<>();
-
- for (String option : options) {
- if (option.startsWith(argument)) {
- completions.add(option);
- }
- }
-
- return completions;
+ return Collections.unmodifiableList(completions);
+ }
+
+ /**
+ * Offer tab completions for a given Enum.
+ *
+ * @param argument the argument to complete
+ * @param enumClazz the Enum to complete for
+ * @return the matching Enum values
+ */
+ public static @NotNull List completeEnum(
+ @NotNull String argument,
+ @NotNull Class extends Enum>> enumClazz
+ ) {
+ argument = argument.toLowerCase(Locale.ENGLISH);
+ List completions = new ArrayList<>();
+
+ for (Enum> enumConstant : enumClazz.getEnumConstants()) {
+ String name = enumConstant.name().toLowerCase(Locale.ENGLISH);
+ if (name.startsWith(argument)) {
+ completions.add(name);
+ }
}
- /**
- * Offer tab completions for visible online Players' names.
- *
- * @param sender the command's sender
- * @param argument the argument to complete
- * @return the matching Players' names
- */
- public static List completeOnlinePlayer(CommandSender sender, String argument) {
- List completions = new ArrayList<>();
- Player senderPlayer = sender instanceof Player ? (Player) sender : null;
-
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (senderPlayer != null && !senderPlayer.canSee(player)) {
- continue;
- }
-
- if (StringUtil.startsWithIgnoreCase(player.getName(), argument)) {
- completions.add(player.getName());
- }
- }
-
- return completions;
+ return completions;
+ }
+
+ /**
+ * Offer tab completions for a given array of Strings.
+ *
+ * @param argument the argument to complete
+ * @param options the Strings which may be completed
+ * @return the matching Strings
+ */
+ public static @NotNull List completeString(
+ @NotNull String argument,
+ @NotNull String @NotNull [] options
+ ) {
+ argument = argument.toLowerCase(Locale.ENGLISH);
+ List completions = new ArrayList<>();
+
+ for (String option : options) {
+ if (option.startsWith(argument)) {
+ completions.add(option);
+ }
}
- /**
- * Offer tab completions for a given array of Objects.
- *
- * @param argument the argument to complete
- * @param converter the Function for converting the Object into a comparable String
- * @param options the Objects which may be completed
- * @return the matching Strings
- */
- public static List completeObject(String argument, Function converter, T[] options) {
- argument = argument.toLowerCase(Locale.ENGLISH);
- List completions = new ArrayList<>();
-
- for (T option : options) {
- String optionString = converter.apply(option).toLowerCase();
- if (optionString.startsWith(argument)) {
- completions.add(optionString);
- }
- }
-
- return completions;
+ return completions;
+ }
+
+ /**
+ * Offer tab completions for visible online Players' names.
+ *
+ * @param sender the command's sender
+ * @param argument the argument to complete
+ * @return the matching Players' names
+ */
+ public static List completeOnlinePlayer(
+ @Nullable CommandSender sender,
+ @NotNull String argument
+ ) {
+ List completions = new ArrayList<>();
+ Player senderPlayer = sender instanceof Player player ? player : null;
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ if (senderPlayer != null && !senderPlayer.canSee(player)) {
+ continue;
+ }
+
+ if (StringUtil.startsWithIgnoreCase(player.getName(), argument)) {
+ completions.add(player.getName());
+ }
}
- private TabCompleter() {}
+ return completions;
+ }
+
+ /**
+ * Offer tab completions for a given array of Objects.
+ *
+ * @param argument the argument to complete
+ * @param converter the Function for converting the Object into a comparable String
+ * @param options the Objects which may be completed
+ * @return the matching Strings
+ */
+ public static List completeObject(
+ @NotNull String argument,
+ @NotNull Function<@NotNull T, @NotNull String> converter,
+ @NotNull T @NotNull[] options
+ ) {
+ argument = argument.toLowerCase(Locale.ENGLISH);
+ List completions = new ArrayList<>();
+
+ for (T option : options) {
+ String optionString = converter.apply(option).toLowerCase(Locale.ENGLISH);
+ if (optionString.startsWith(argument)) {
+ completions.add(optionString);
+ }
+ }
+
+ return completions;
+ }
+
+ private TabCompleter() {
+ throw new IllegalStateException("Cannot create instance of utility class.");
+ }
}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/config/Config.java b/plugin/src/main/java/com/lishid/openinv/util/config/Config.java
new file mode 100644
index 00000000..ad5f8f92
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/config/Config.java
@@ -0,0 +1,43 @@
+package com.lishid.openinv.util.config;
+
+import com.lishid.openinv.util.AccessEqualMode;
+import org.bukkit.configuration.Configuration;
+import org.bukkit.configuration.MemoryConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class Config {
+
+ private @NotNull Configuration root;
+ private @Nullable AccessEqualMode accessEqualMode;
+
+ public Config() {
+ root = new MemoryConfiguration();
+ }
+
+ public void reload(@NotNull Configuration configuration) {
+ root = configuration;
+ accessEqualMode = null;
+ }
+
+ public boolean isSaveDisabled() {
+ return root.getBoolean("settings.disable-saving", false);
+ }
+
+ public boolean isOfflineDisabled() {
+ return root.getBoolean("settings.disable-offline-access", false);
+ }
+
+ public boolean doesNoArgsOpenSelf() {
+ return root.getBoolean("settings.command.open.no-args-opens-self", false);
+ }
+
+ public @NotNull AccessEqualMode getAccessEqualMode() {
+ if (accessEqualMode == null) {
+ accessEqualMode = AccessEqualMode.of(root.getString("settings.equal-access"));
+ }
+
+ return accessEqualMode;
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java b/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java
new file mode 100644
index 00000000..228cfd07
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java
@@ -0,0 +1,129 @@
+/*
+ * 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.util.config;
+
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.plugin.Plugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.IOException;
+
+public record ConfigUpdater(@NotNull Plugin plugin) {
+
+ public void checkForUpdates() {
+ final int version = plugin.getConfig().getInt("config-version", 1);
+ ConfigurationSection defaults = plugin.getConfig().getDefaults();
+ if (defaults == null || version >= defaults.getInt("config-version")) {
+ return;
+ }
+
+ plugin.getLogger().info("Configuration update found! Performing update...");
+
+ // Backup the old config file
+ try {
+ plugin.getConfig().save(new File(plugin.getDataFolder(), "config_old.yml"));
+ plugin.getLogger().info("Backed up config.yml to config_old.yml before updating.");
+ } catch (IOException e) {
+ plugin.getLogger().warning("Could not back up config.yml before updating!");
+ }
+
+ if (version < 2) {
+ updateConfig1To2();
+ }
+ if (version < 3) {
+ updateConfig2To3();
+ }
+ if (version < 4) {
+ updateConfig3To4();
+ }
+ if (version < 5) {
+ updateConfig4To5();
+ }
+ if (version < 6) {
+ updateConfig5To6();
+ }
+ if (version < 7) {
+ updateConfig6To7();
+ }
+ if (version < 8) {
+ updateConfig7To8();
+ }
+
+ plugin.saveConfig();
+ plugin.getLogger().info("Configuration update complete!");
+ }
+
+ private void updateConfig7To8() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("settings.equal-access", "view");
+ config.set("config-version", 8);
+ }
+
+ private void updateConfig6To7() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("toggles", null);
+ String consoleLocale = config.getString("settings.locale", "en");
+ if (consoleLocale.isBlank() || consoleLocale.equalsIgnoreCase("en_us")) {
+ consoleLocale = "en";
+ }
+ config.set("settings.console-locale", consoleLocale);
+ config.set("settings.locale", null);
+ config.set("config-version", 7);
+ }
+
+ private void updateConfig5To6() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("settings.command.open.no-args-opens-self", false);
+ config.set("settings.command.searchcontainer.max-radius", 10);
+ config.set("config-version", 6);
+ }
+
+ private void updateConfig4To5() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("settings.disable-offline-access", false);
+ config.set("config-version", 5);
+ }
+
+ private void updateConfig3To4() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("notify", null);
+ config.set("config-version", 4);
+ }
+
+ private void updateConfig2To3() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("items", null);
+ config.set("ItemOpenInv", null);
+ config.set("toggles", null);
+ config.set("settings.disable-saving", config.getBoolean("DisableSaving", false));
+ config.set("DisableSaving", null);
+ config.set("config-version", 3);
+ }
+
+ private void updateConfig1To2() {
+ FileConfiguration config = plugin.getConfig();
+ config.set("ItemOpenInvItemID", null);
+ config.set("NotifySilentChest", null);
+ config.set("NotifyAnyChest", null);
+ config.set("AnyChest", null);
+ config.set("SilentChest", null);
+ config.set("config-version", 2);
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java b/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java
new file mode 100644
index 00000000..2ae68159
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java
@@ -0,0 +1,87 @@
+package com.lishid.openinv.util.lang;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class LangMigrator {
+
+ private final @NotNull Path oldFolder;
+ private final @NotNull Path newFolder;
+ private final @NotNull Logger logger;
+
+ public LangMigrator(@NotNull Path oldFolder, @NotNull Path newFolder, @NotNull Logger logger) {
+ this.oldFolder = oldFolder;
+ this.newFolder = newFolder;
+ this.logger = logger;
+ }
+
+ public void migrate() {
+ if (!Files.exists(oldFolder.resolve("en_us.yml"))) {
+ // Probably already migrated.
+ return;
+ }
+
+ logger.info(() -> String.format("[LanguageManager] Migrating language files to %s", newFolder));
+
+ if (!Files.exists(newFolder)) {
+ try {
+ Files.createDirectories(newFolder);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Unable to create language subdirectory!", e);
+ }
+ }
+
+ try (DirectoryStream files = Files.newDirectoryStream(oldFolder)) {
+ files.forEach(path -> {
+ if (path == null) {
+ return;
+ }
+
+ String fileName = path.getFileName().toString();
+
+ if (fileName.startsWith("config") || !fileName.endsWith(".yml")) {
+ return;
+ }
+
+ // Migrate certain files to be parent languages.
+ fileName = switch (fileName) {
+ case "en_us.yml" -> "en.yml";
+ case "de_de.yml" -> "de.yml";
+ case "es_es.yml" -> "es.yml";
+ case "pt_br.yml" -> "pt.yml";
+ default -> fileName;
+ };
+
+ try {
+ Files.copy(path, newFolder.resolve(fileName));
+ Files.delete(path);
+ } catch (FileAlreadyExistsException e1) {
+ // File already migrated?
+ try {
+ Files.copy(path, newFolder.resolve("old_" + fileName));
+ Files.delete(path);
+ } catch (IOException e2) {
+ // If it fails again, just re-throw.
+ throw new UncheckedIOException(e2);
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ });
+ } catch (UncheckedIOException e) {
+ logger.log(Level.WARNING, "Unable to migrate languages to subdirectory!", e.getCause());
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "Unable to migrate languages to subdirectory!", e);
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml
index d8bc7bb0..a69d6d62 100644
--- a/plugin/src/main/resources/config.yml
+++ b/plugin/src/main/resources/config.yml
@@ -1,4 +1,11 @@
-config-version: 4
+config-version: 8
settings:
+ equal-access: allow
+ command:
+ open:
+ no-args-opens-self: false
+ searchcontainer:
+ max-radius: 10
+ disable-offline-access: false
disable-saving: false
- locale: 'en_us'
+ console-locale: 'en'
diff --git a/plugin/src/main/resources/de_de.yml b/plugin/src/main/resources/locale/de.yml
similarity index 92%
rename from plugin/src/main/resources/de_de.yml
rename to plugin/src/main/resources/locale/de.yml
index ea1d0dce..e866e3f9 100644
--- a/plugin/src/main/resources/de_de.yml
+++ b/plugin/src/main/resources/locale/de.yml
@@ -23,8 +23,5 @@ messages:
container:
noMatches: 'Keine Container mit %target% gefunden.'
matches: 'Container hat %target%: %detail%'
- on: 'an'
- off: 'aus'
-container:
- player: '%player%''s Inventar'
- enderchest: '%player%''s Endertruhe'
+ 'on': 'an'
+ 'off': 'aus'
diff --git a/plugin/src/main/resources/en_us.yml b/plugin/src/main/resources/locale/en.yml
similarity index 72%
rename from plugin/src/main/resources/en_us.yml
rename to plugin/src/main/resources/locale/en.yml
index 58e66f2c..287c83da 100644
--- a/plugin/src/main/resources/en_us.yml
+++ b/plugin/src/main/resources/locale/en.yml
@@ -6,16 +6,19 @@ messages:
invalidNumber: '&cInvalid number: "%target%"'
invalidPlayer: '&cPlayer not found!'
permissionOpenSelf: '&cYou''re not allowed to open your own inventory.'
- permissionEnderAll: '&cYou''re not allowed to access other players'' ender chests.'
+ permissionOpenOther: '&cYou''re not allowed to access others'' inventories.'
permissionExempt: '&c%target%''s inventory is protected.'
permissionCrossWorld: '&c%target% is not in your world.'
- permissionPlayerOnline: '&cYou''re not allowed to open the inventory of online players.'
- permissionPlayerOffline: '&cYou''re not allowed to open the inventory of offline players.'
+ permissionPlayerOnline: '&cYou''re not allowed to open inventories of online players.'
+ permissionPlayerOffline: '&cYou''re not allowed to open inventories of offline players.'
commandException: '&cAn error occurred. Please check console for details.'
info:
containerBlocked: 'You are opening a blocked container.'
containerBlockedSilent: 'You are opening a blocked container silently.'
containerSilent: 'You are opening a container silently.'
+ clear:
+ inventory: 'Cleared %target%''s inventory.'
+ enderchest: 'Cleared %target%''s ender chest.'
settingState: '%setting%: %state%'
player:
noMatches: 'No players found with %target%.'
@@ -23,8 +26,5 @@ messages:
container:
noMatches: 'No containers found with %target%.'
matches: 'Containers holding %target%: %detail%'
- on: 'on'
- off: 'off'
-container:
- player: '%player%''s Inventory'
- enderchest: '%player%''s Ender Chest'
+ 'on': 'on'
+ 'off': 'off'
diff --git a/plugin/src/main/resources/es_es.yml b/plugin/src/main/resources/locale/es.yml
similarity index 91%
rename from plugin/src/main/resources/es_es.yml
rename to plugin/src/main/resources/locale/es.yml
index 41541fae..d4a2b69c 100644
--- a/plugin/src/main/resources/es_es.yml
+++ b/plugin/src/main/resources/locale/es.yml
@@ -23,9 +23,5 @@ messages:
container:
noMatches: 'No se encontraron contenedores con %target%.'
matches: 'Contenedores con %target%: %detail%'
- on: 'activado'
- off: 'desactivado'
-container:
- player: 'Inventario de %player%'
- enderchest: 'Cofre de Ender de %player%'
-
\ No newline at end of file
+ 'on': 'activado'
+ 'off': 'desactivado'
diff --git a/plugin/src/main/resources/pt_br.yml b/plugin/src/main/resources/locale/pt.yml
similarity index 92%
rename from plugin/src/main/resources/pt_br.yml
rename to plugin/src/main/resources/locale/pt.yml
index cd26a2d7..d0cd5d89 100644
--- a/plugin/src/main/resources/pt_br.yml
+++ b/plugin/src/main/resources/locale/pt.yml
@@ -23,8 +23,5 @@ messages:
container:
noMatches: 'Nenhum recipiente encontrado com %target%.'
matches: 'Recipientes contendo %target%: %detail%'
- on: 'ligado'
- off: 'desligado'
-container:
- player: 'Inventario de %player%'
- enderchest: 'Bau de Ender de %player%'
+ 'on': 'ligado'
+ 'off': 'desligado'
diff --git a/plugin/src/main/resources/locale/zh_cn.yml b/plugin/src/main/resources/locale/zh_cn.yml
new file mode 100644
index 00000000..ab0d34cb
--- /dev/null
+++ b/plugin/src/main/resources/locale/zh_cn.yml
@@ -0,0 +1,28 @@
+# Translated into Chinese Simplified by Flandre_tw
+messages:
+ error:
+ consoleUnsupported: 该命令无法在后台执行。
+ lootNotGenerated: '&c奖励箱尚未生成 ! 请关闭 &b/silentcontainer&c。'
+ invalidMaterial: '&c无效的物品 : "%target%"'
+ invalidNumber: '&c无效的数字 : "%target%"'
+ invalidPlayer: '&c玩家不存在 !'
+ permissionOpenSelf: '&c你无法开启自己的物品栏。'
+ permissionEnderAll: '&c你无法开启其他玩家的末影箱。'
+ permissionExempt: '&c%target% 的物品栏受到保护。'
+ permissionCrossWorld: '&c%target% 不在你所在的世界。'
+ permissionPlayerOnline: '&c你无法开启线上玩家的物品栏。'
+ permissionPlayerOffline: '&c你无法开启离线玩家的物品栏。'
+ commandException: '&c发生错误,请查看后台。'
+ info:
+ containerBlocked: 你正在开启受阻挡的储物箱。
+ containerBlockedSilent: 你正在悄悄开启受阻挡的储物箱。
+ containerSilent: 你正在悄悄开启储物箱。
+ settingState: '%setting% : %state%'
+ player:
+ noMatches: 找不到持有 %target% 的玩家。
+ matches: '找到持有 %target% 的玩家 : %detail%'
+ container:
+ noMatches: 找不到放有 %target% 的储物箱。
+ matches: '找到放有 %target% 的储物箱 : %detail%'
+ 'on': '开启'
+ 'off': '关闭'
diff --git a/plugin/src/main/resources/locale/zh_tw.yml b/plugin/src/main/resources/locale/zh_tw.yml
new file mode 100644
index 00000000..b6a16d26
--- /dev/null
+++ b/plugin/src/main/resources/locale/zh_tw.yml
@@ -0,0 +1,28 @@
+# Translated into Chinese Traditional by Flandre_tw
+messages:
+ error:
+ consoleUnsupported: 該指令無法在控制台執行。
+ lootNotGenerated: '&c獎勵箱尚未生成 ! 請關閉 &b/silentcontainer&c。'
+ invalidMaterial: '&c無效的物品 : "%target%"'
+ invalidNumber: '&c無效的數字 : "%target%"'
+ invalidPlayer: '&c玩家不存在 !'
+ permissionOpenSelf: '&c你無法開啟自己的物品欄。'
+ permissionEnderAll: '&c你無法開啟其他玩家的終界箱。'
+ permissionExempt: '&c%target% 的物品欄受到保護。'
+ permissionCrossWorld: '&c%target% 不在你所在的世界。'
+ permissionPlayerOnline: '&c你無法開啟線上玩家的物品欄。'
+ permissionPlayerOffline: '&c你無法開啟離線玩家的物品欄。'
+ commandException: '&c發生錯誤,請查看控制台。'
+ info:
+ containerBlocked: 你正在開啟受阻擋的儲物箱。
+ containerBlockedSilent: 你正在悄悄開啟受阻擋的儲物箱。
+ containerSilent: 你正在悄悄開啟儲物箱。
+ settingState: '%setting% : %state%'
+ player:
+ noMatches: 找不到持有 %target% 的玩家。
+ matches: '找到持有 %target% 的玩家 : %detail%'
+ container:
+ noMatches: 找不到放有 %target% 的儲物箱。
+ matches: '找到放有 %target% 的儲物箱 : %detail%'
+ 'on': '開啟'
+ 'off': '關閉'
diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml
index 767b886d..6ec9d2ef 100644
--- a/plugin/src/main/resources/plugin.yml
+++ b/plugin/src/main/resources/plugin.yml
@@ -1,89 +1,152 @@
name: OpenInv
main: com.lishid.openinv.OpenInv
-version: ${project.version}
+version: ${version}
author: lishid
-authors: [Jikoo, ShadowRanger]
-description: >
- This plugin allows you to open a player's inventory as a chest and interact with it in real time.
-api-version: "1.16"
+authors: [ Jikoo, ShadowRanger ]
+description: Open a player's inventory as a chest and interact with it in real time.
+api-version: "1.13"
+folia-supported: true
permissions:
- OpenInv.any.default:
- description: Permission for AnyContainer to default on prior to toggling.
- default: false
- OpenInv.silent.default:
- description: Permission for SilentContainer to default on prior to toggling.
- default: false
- OpenInv.*:
- description: Permission for all OpenInv features.
- default: op
- children:
- OpenInv.openinv: true
- OpenInv.openender: true
- OpenInv.search: true
- OpenInv.silent: true
- OpenInv.anychest: true
- OpenInv.searchenchant: true
- OpenInv.searchcontainer: true
- OpenInv.openonline: true
- OpenInv.openoffline: true
- OpenInv.spectate: true
- OpenInv.openinv:
- default: op
- children:
- OpenInv.openonline: true
- OpenInv.openoffline: true
- OpenInv.openender:
- default: op
+
+ openinv:
children:
- OpenInv.openonline: true
- OpenInv.openoffline: true
+ # Inventory nodes (/openinv)
+ openinv.inventory:
+ children:
+ openinv.inventory.open:
+ children:
+ openinv.inventory.open.self: true
+ openinv.inventory.open.other: true
+ openinv.inventory.edit:
+ children:
+ openinv.inventory.open: true
+ openinv.inventory.edit.self:
+ children:
+ openinv.inventory.open.self: true
+ openinv.inventory.edit.other:
+ children:
+ openinv.inventory.open.other: true
+ # Specific slot behaviors inside opened player inventories
+ openinv.inventory.slot:
+ default: true
+ children:
+ openinv.inventory.slot.head.any: true
+ openinv.inventory.slot.chest.any: true
+ openinv.inventory.slot.legs.any: true
+ openinv.inventory.slot.feet.any: true
+ openinv.inventory.slot.drop: true
+ # Ender chest nodes (/openender)
+ openinv.enderchest:
+ children:
+ openinv.enderchest.open:
+ children:
+ openinv.enderchest.open.self: true
+ openinv.enderchest.open.other: true
+ openinv.enderchest.edit:
+ children:
+ openinv.enderchest.edit.self:
+ children:
+ openinv.enderchest.open.self: true
+ openinv.enderchest.edit.other:
+ children:
+ openinv.enderchest.open.other: true
+ # Clear nodes (/clearinv and /clearender)
+ openinv.clear:
+ children:
+ openinv.clear.self: true
+ openinv.clear.other: true
+ # Player access
+ openinv.access:
+ children:
+ openinv.access.offline: true
+ openinv.access.online: true
+ openinv.access.crossworld: true
+ openinv.access.level.1: true
+ openinv.access.level.2: false
+ openinv.access.level.3: false
+ openinv.access.level.4: false
+ openinv.access.equal.edit: false
+ openinv.access.equal.view: false
+ openinv.access.equal.deny: false
+ # Spectate features
+ openinv.spectate:
+ children:
+ openinv.spectate.click: true
+ # Container features
+ openinv.container:
+ children:
+ openinv.container.any:
+ children:
+ openinv.container.any.use: true
+ openinv.container.any.use:
+ default: false
+ openinv.container.silent:
+ children:
+ openinv.container.silent.use: true
+ openinv.container.silent.use:
+ default: false
+ # Search functionality
+ openinv.search:
+ children:
+ openinv.search.inventory: true
+ openinv.search.container: true
commands:
openinv:
- aliases: [oi, inv, open]
+ aliases: [ oi, inv, open ]
description: Open a player's inventory
- permission: OpenInv.openinv
+ permission: openinv.inventory.open.self;openinv.inventory.open.other
+ usage: |-
+ / [Player] - Open a player's inventory
+ clearinv:
+ description: Clear a player's inventory
+ permission: openinv.clear.self;openinv.clear.other
usage: |-
- / [Player] - Open a player's inventory
+ / [Player] - Clear a player's inventory
openender:
- aliases: [oe]
- description: Opens the enderchest of a player
- permission: OpenInv.openender
+ aliases: [ oe ]
+ description: Open a player's ender chest
+ permission: openinv.enderchest.open.self;openinv.enderchest.open.other
+ usage: |-
+ / [Player] - Open a player's ender chest
+ clearender:
+ description: Clear a player's ender chest
+ permission: openinv.clear.self;openinv.clear.other
usage: |-
- / [Player] - Open a player's enderchest
+ / [Player] - Clear a player's ender chest
searchinv:
- aliases: [si]
+ aliases: [ si ]
description: Search and list players having a specific item
- permission: OpenInv.search
+ permission: openinv.search.inventory
usage: |-
- / [MinAmount] - MinAmount is optional, the minimum amount required
+ / [MinAmount] - MinAmount is optional, the minimum amount required
searchender:
- aliases: [se]
- permission: OpenInv.search
- description: Searches and lists players having a specific item in their ender chest
+ aliases: [ se ]
+ permission: openinv.search.inventory
+ description: Search and list players having a specific item in their ender chest
usage: |-
- / [MinAmount] - MinAmount is optional, the minimum amount required
+ / [MinAmount] - MinAmount is optional, the minimum amount required
silentcontainer:
- aliases: [sc, silent, silentchest]
+ aliases: [ sc, silent, silentchest ]
description: SilentContainer stops sounds and animations when using containers.
- permission: OpenInv.silent
+ permission: openinv.container.silent
usage: |-
- / [check|on|off] - Check, toggle, or set SilentContainer
+ / [check|on|off] - Check, toggle, or set SilentContainer
anycontainer:
- aliases: [ac, anychest]
+ aliases: [ ac, anychest ]
description: AnyContainer allows using blocked containers.
- permission: OpenInv.anychest
+ permission: openinv.container.any
usage: |-
- / [check|on|off] - Check, toggle, or set AnyContainer
+ / [check|on|off] - Check, toggle, or set AnyContainer
searchenchant:
- aliases: [searchenchants]
+ aliases: [ searchenchants ]
description: Search and list players with a specific enchantment.
- permission: OpenInv.searchenchant
+ permission: openinv.search.inventory
usage: |-
- / <[Enchantment] [MinLevel]> - Enchantment is the enchantment type, MinLevel is the minimum level. One is optional
+ / <[Enchantment] [MinLevel]> - Enchantment is the enchantment type, MinLevel is the minimum level. One is optional
searchcontainer:
- aliases: [searchchest]
+ aliases: [ searchchest ]
description: Search and list containers with a specific material.
- permission: OpenInv.searchcontainer
+ permission: openinv.search.container
usage: / [ChunkRadius] - ChunkRadius is optional, the length that will be searched for matching items. Default 5
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index 6882a320..00000000
--- a/pom.xml
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
- 4.0.0
-
- com.lishid
- openinvparent
- OpenInvParent
- http://dev.bukkit.org/bukkit-plugins/openinv/
- 4.1.6-SNAPSHOT
-
- pom
-
-
- UTF-8
-
-
-
- api
- plugin
- internal
- assembly
-
-
-
-
-
-
-
- all
-
-
- all
- true
-
-
-
-
-
-
-
-
- spigot-repo
- https://hub.spigotmc.org/nexus/content/groups/public/
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.2.2
-
-
-
- *:*
-
-
- META-INF/maven/**
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
-
- maven-compiler-plugin
- 3.8.1
-
- 1.8
- 1.8
-
-
-
-
-
-
diff --git a/resource-pack/build.gradle.kts b/resource-pack/build.gradle.kts
new file mode 100644
index 00000000..f94f30ea
--- /dev/null
+++ b/resource-pack/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ `base`
+}
+
+tasks.register("buildResourcePack") {
+ archiveFileName = "openinv-legibility-pack.zip"
+ destinationDirectory = rootProject.layout.projectDirectory.dir("dist")
+
+ from("openinv-legibility-pack")
+ with(copySpec {
+ include("**/*.json", "**/*.png", "pack.mcmeta")
+ })
+}
+
+tasks.assemble {
+ dependsOn(tasks.named("buildResourcePack"))
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json
new file mode 100644
index 00000000..482b3af9
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json
@@ -0,0 +1,25 @@
+{
+ "texture_size": [ 16, 32 ],
+ "textures": {
+ "layer0": "openinv:item/crafting_output",
+ "particle": "minecraft:block/crafting_table_front"
+ },
+ "elements": [
+ {
+ "from": [ 0, -16, -16 ],
+ "to": [ 16, 16, -16 ],
+ "faces": {
+ "south": {
+ "uv": [ 0, 0, 16, 16 ],
+ "texture": "#layer0"
+ }
+ }
+ }
+ ],
+ "gui_light": "front",
+ "display": {
+ "gui": {
+ "scale": [ 1.125, 1.125, 1 ]
+ }
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json
new file mode 100644
index 00000000..6c033d83
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/cursor"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json
new file mode 100644
index 00000000..b4782491
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/drop"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json
new file mode 100644
index 00000000..bdb545f8
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/empty_boots"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json
new file mode 100644
index 00000000..b407d98c
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/empty_chestplate"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json
new file mode 100644
index 00000000..f7cc30f6
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/empty_helmet"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json
new file mode 100644
index 00000000..0467df35
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/empty_leggings"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json
new file mode 100644
index 00000000..0cf9047a
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/empty_shield"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json
new file mode 100644
index 00000000..a4955524
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json
@@ -0,0 +1,11 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "openinv:item/not_a_slot"
+ },
+ "display": {
+ "gui": {
+ "scale": [ 1.125, -1.125, -1.125 ]
+ }
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png
new file mode 100644
index 00000000..a53a4142
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png
new file mode 100644
index 00000000..af54bc59
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png
new file mode 100644
index 00000000..8caf814e
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png
new file mode 100644
index 00000000..356e615b
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png
new file mode 100644
index 00000000..be0e904a
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png
new file mode 100644
index 00000000..59c44691
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png
new file mode 100644
index 00000000..bea579e0
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png
new file mode 100644
index 00000000..35e73102
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png differ
diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png
new file mode 100644
index 00000000..1d78e569
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png differ
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json
new file mode 100644
index 00000000..230469c5
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json
@@ -0,0 +1,11 @@
+{
+ "parent": "minecraft:block/crafting_table",
+ "overrides": [
+ {
+ "model": "openinv:item/crafting_output",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ }
+ ]
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json
new file mode 100644
index 00000000..0c8bb744
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json
@@ -0,0 +1,11 @@
+{
+ "parent": "minecraft:block/dropper",
+ "overrides": [
+ {
+ "model": "openinv:item/drop",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ }
+ ]
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json
new file mode 100644
index 00000000..f9cd4073
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json
@@ -0,0 +1,75 @@
+{
+ "parent": "minecraft:item/generated",
+ "overrides": [
+ {
+ "model": "openinv:item/empty_boots",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_quartz_trim",
+ "predicate": {
+ "trim_type": 0.1
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_iron_trim",
+ "predicate": {
+ "trim_type": 0.2
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_netherite_trim",
+ "predicate": {
+ "trim_type": 0.3
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_redstone_trim",
+ "predicate": {
+ "trim_type": 0.4
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_copper_trim",
+ "predicate": {
+ "trim_type": 0.5
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_gold_trim",
+ "predicate": {
+ "trim_type": 0.6
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_emerald_trim",
+ "predicate": {
+ "trim_type": 0.7
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_diamond_trim",
+ "predicate": {
+ "trim_type": 0.8
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_lapis_trim",
+ "predicate": {
+ "trim_type": 0.9
+ }
+ },
+ {
+ "model": "minecraft:item/leather_boots_amethyst_trim",
+ "predicate": {
+ "trim_type": 1.0
+ }
+ }
+ ],
+ "textures": {
+ "layer0": "minecraft:item/leather_boots",
+ "layer1": "minecraft:item/leather_boots_overlay"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json
new file mode 100644
index 00000000..d6dc8c5f
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json
@@ -0,0 +1,75 @@
+{
+ "parent": "minecraft:item/generated",
+ "overrides": [
+ {
+ "model": "openinv:item/empty_chestplate",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_quartz_trim",
+ "predicate": {
+ "trim_type": 0.1
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_iron_trim",
+ "predicate": {
+ "trim_type": 0.2
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_netherite_trim",
+ "predicate": {
+ "trim_type": 0.3
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_redstone_trim",
+ "predicate": {
+ "trim_type": 0.4
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_copper_trim",
+ "predicate": {
+ "trim_type": 0.5
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_gold_trim",
+ "predicate": {
+ "trim_type": 0.6
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_emerald_trim",
+ "predicate": {
+ "trim_type": 0.7
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_diamond_trim",
+ "predicate": {
+ "trim_type": 0.8
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_lapis_trim",
+ "predicate": {
+ "trim_type": 0.9
+ }
+ },
+ {
+ "model": "minecraft:item/leather_chestplate_amethyst_trim",
+ "predicate": {
+ "trim_type": 1.0
+ }
+ }
+ ],
+ "textures": {
+ "layer0": "minecraft:item/leather_chestplate",
+ "layer1": "minecraft:item/leather_chestplate_overlay"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json
new file mode 100644
index 00000000..236ae610
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json
@@ -0,0 +1,75 @@
+{
+ "parent": "minecraft:item/generated",
+ "overrides": [
+ {
+ "model": "openinv:item/empty_helmet",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_quartz_trim",
+ "predicate": {
+ "trim_type": 0.1
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_iron_trim",
+ "predicate": {
+ "trim_type": 0.2
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_netherite_trim",
+ "predicate": {
+ "trim_type": 0.3
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_redstone_trim",
+ "predicate": {
+ "trim_type": 0.4
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_copper_trim",
+ "predicate": {
+ "trim_type": 0.5
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_gold_trim",
+ "predicate": {
+ "trim_type": 0.6
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_emerald_trim",
+ "predicate": {
+ "trim_type": 0.7
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_diamond_trim",
+ "predicate": {
+ "trim_type": 0.8
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_lapis_trim",
+ "predicate": {
+ "trim_type": 0.9
+ }
+ },
+ {
+ "model": "minecraft:item/leather_helmet_amethyst_trim",
+ "predicate": {
+ "trim_type": 1.0
+ }
+ }
+ ],
+ "textures": {
+ "layer0": "minecraft:item/leather_helmet",
+ "layer1": "minecraft:item/leather_helmet_overlay"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json
new file mode 100644
index 00000000..eb9ddc89
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json
@@ -0,0 +1,75 @@
+{
+ "parent": "minecraft:item/generated",
+ "overrides": [
+ {
+ "model": "openinv:item/empty_leggings",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_quartz_trim",
+ "predicate": {
+ "trim_type": 0.1
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_iron_trim",
+ "predicate": {
+ "trim_type": 0.2
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_netherite_trim",
+ "predicate": {
+ "trim_type": 0.3
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_redstone_trim",
+ "predicate": {
+ "trim_type": 0.4
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_copper_trim",
+ "predicate": {
+ "trim_type": 0.5
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_gold_trim",
+ "predicate": {
+ "trim_type": 0.6
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_emerald_trim",
+ "predicate": {
+ "trim_type": 0.7
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_diamond_trim",
+ "predicate": {
+ "trim_type": 0.8
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_lapis_trim",
+ "predicate": {
+ "trim_type": 0.9
+ }
+ },
+ {
+ "model": "minecraft:item/leather_leggings_amethyst_trim",
+ "predicate": {
+ "trim_type": 1.0
+ }
+ }
+ ],
+ "textures": {
+ "layer0": "minecraft:item/leather_leggings",
+ "layer1": "minecraft:item/leather_leggings_overlay"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json
new file mode 100644
index 00000000..5ea7eddd
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json
@@ -0,0 +1,58 @@
+{
+ "parent": "builtin/entity",
+ "gui_light": "front",
+ "textures": {
+ "particle": "block/dark_oak_planks"
+ },
+ "display": {
+ "thirdperson_righthand": {
+ "rotation": [ 0, 90, 0 ],
+ "translation": [ 10, 6, -4 ],
+ "scale": [ 1, 1, 1 ]
+ },
+ "thirdperson_lefthand": {
+ "rotation": [ 0, 90, 0 ],
+ "translation": [ 10, 6, 12 ],
+ "scale": [ 1, 1, 1 ]
+ },
+ "firstperson_righthand": {
+ "rotation": [ 0, 180, 5 ],
+ "translation": [ -10, 2, -10 ],
+ "scale": [ 1.25, 1.25, 1.25 ]
+ },
+ "firstperson_lefthand": {
+ "rotation": [ 0, 180, 5 ],
+ "translation": [ 10, 0, -10 ],
+ "scale": [ 1.25, 1.25, 1.25 ]
+ },
+ "gui": {
+ "rotation": [ 15, -25, -5 ],
+ "translation": [ 2, 3, 0 ],
+ "scale": [ 0.65, 0.65, 0.65 ]
+ },
+ "fixed": {
+ "rotation": [ 0, 180, 0 ],
+ "translation": [ -4.5, 4.5, -5],
+ "scale":[ 0.55, 0.55, 0.55]
+ },
+ "ground": {
+ "rotation": [ 0, 0, 0 ],
+ "translation": [ 2, 4, 2],
+ "scale":[ 0.25, 0.25, 0.25]
+ }
+ },
+ "overrides": [
+ {
+ "model": "openinv:item/empty_shield",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ },
+ {
+ "predicate": {
+ "blocking": 1
+ },
+ "model": "item/shield_blocking"
+ }
+ ]
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json
new file mode 100644
index 00000000..bc6fadb8
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json
@@ -0,0 +1,11 @@
+{
+ "parent": "minecraft:item/template_banner",
+ "overrides": [
+ {
+ "model": "openinv:item/cursor",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ }
+ ]
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json
new file mode 100644
index 00000000..e4edacdb
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json
@@ -0,0 +1,14 @@
+{
+ "parent": "minecraft:item/generated",
+ "textures": {
+ "layer0": "minecraft:block/white_stained_glass"
+ },
+ "overrides": [
+ {
+ "model": "openinv:item/not_a_slot",
+ "predicate": {
+ "custom_model_data": 9999
+ }
+ }
+ ]
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json
new file mode 100644
index 00000000..1e24a414
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json
@@ -0,0 +1,20 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/crafting_output"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:block/crafting_table"
+ },
+ "property": "minecraft:custom_model_data"
+ },
+ "oversized_in_gui": true
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json
new file mode 100644
index 00000000..8493cfa9
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json
@@ -0,0 +1,19 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/drop"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:block/dropper"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json
new file mode 100644
index 00000000..c6859113
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json
@@ -0,0 +1,174 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/empty_boots"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_quartz_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:quartz"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_iron_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:iron"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_netherite_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:netherite"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_redstone_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:redstone"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_copper_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:copper"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_gold_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:gold"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_emerald_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:emerald"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_diamond_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:diamond"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_lapis_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:lapis"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_amethyst_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:amethyst"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots_resin_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:resin"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_boots",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "property": "minecraft:trim_material"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json
new file mode 100644
index 00000000..10e51d04
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json
@@ -0,0 +1,174 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/empty_chestplate"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_quartz_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:quartz"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_iron_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:iron"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_netherite_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:netherite"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_redstone_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:redstone"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_copper_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:copper"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_gold_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:gold"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_emerald_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:emerald"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_diamond_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:diamond"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_lapis_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:lapis"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_amethyst_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:amethyst"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate_resin_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:resin"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_chestplate",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "property": "minecraft:trim_material"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json
new file mode 100644
index 00000000..27a56391
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json
@@ -0,0 +1,174 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/empty_helmet"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_quartz_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:quartz"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_iron_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:iron"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_netherite_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:netherite"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_redstone_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:redstone"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_copper_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:copper"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_gold_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:gold"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_emerald_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:emerald"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_diamond_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:diamond"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_lapis_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:lapis"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_amethyst_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:amethyst"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet_resin_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:resin"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_helmet",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "property": "minecraft:trim_material"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json
new file mode 100644
index 00000000..4686e060
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json
@@ -0,0 +1,174 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/empty_leggings"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_quartz_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:quartz"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_iron_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:iron"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_netherite_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:netherite"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_redstone_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:redstone"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_copper_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:copper"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_gold_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:gold"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_emerald_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:emerald"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_diamond_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:diamond"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_lapis_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:lapis"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_amethyst_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:amethyst"
+ },
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings_resin_trim",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "when": "minecraft:resin"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:model",
+ "model": "minecraft:item/leather_leggings",
+ "tints": [
+ {
+ "type": "minecraft:dye",
+ "default": -6265536
+ }
+ ]
+ },
+ "property": "minecraft:trim_material"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json
new file mode 100644
index 00000000..cfac9c50
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json
@@ -0,0 +1,33 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/empty_shield"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:condition",
+ "on_false": {
+ "type": "minecraft:special",
+ "base": "minecraft:item/shield",
+ "model": {
+ "type": "minecraft:shield"
+ }
+ },
+ "on_true": {
+ "type": "minecraft:special",
+ "base": "minecraft:item/shield_blocking",
+ "model": {
+ "type": "minecraft:shield"
+ }
+ },
+ "property": "minecraft:using_item"
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json
new file mode 100644
index 00000000..9f3d690e
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json
@@ -0,0 +1,23 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "minecraft:model",
+ "model": "openinv:item/cursor"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "minecraft:special",
+ "base": "minecraft:item/template_banner",
+ "model": {
+ "type": "minecraft:banner",
+ "color": "white"
+ }
+ },
+ "property": "minecraft:custom_model_data"
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json
new file mode 100644
index 00000000..59f6d812
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json
@@ -0,0 +1,20 @@
+{
+ "model": {
+ "type": "minecraft:select",
+ "cases": [
+ {
+ "model": {
+ "type": "model",
+ "model": "openinv:item/not_a_slot"
+ },
+ "when": "openinv:custom"
+ }
+ ],
+ "fallback": {
+ "type": "model",
+ "model": "item/white_stained_glass_pane"
+ },
+ "property": "custom_model_data"
+ },
+ "oversized_in_gui": true
+}
diff --git a/resource-pack/openinv-legibility-pack/pack.mcmeta b/resource-pack/openinv-legibility-pack/pack.mcmeta
new file mode 100644
index 00000000..a8a915fc
--- /dev/null
+++ b/resource-pack/openinv-legibility-pack/pack.mcmeta
@@ -0,0 +1,23 @@
+{
+ "pack": {
+ "description": "Improve OpenInv's legibility",
+ "min_format": 34,
+ "max_format": [75, 0],
+ "pack_format": 64,
+ "supported_formats": [ 34, 75 ]
+ },
+ "overlays": {
+ "entries": [
+ {
+ "min_format": 44,
+ "max_format": [75, 0],
+ "formats": [ 44, 75 ],
+ "directory": "openinv_44"
+ },
+ {
+ "formats": [ 34, 43 ],
+ "directory": "openinv_34"
+ }
+ ]
+ }
+}
diff --git a/resource-pack/openinv-legibility-pack/pack.png b/resource-pack/openinv-legibility-pack/pack.png
new file mode 100644
index 00000000..536a28f6
Binary files /dev/null and b/resource-pack/openinv-legibility-pack/pack.png differ
diff --git a/scripts/generate_changelog.sh b/scripts/generate_changelog.sh
deleted file mode 100644
index 72c823d9..00000000
--- a/scripts/generate_changelog.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/bin/bash
-#
-# 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 .
-#
-
-# A script for generating a changelog from Git.
-#
-# Note that this script is designed for use in GitHub Actions, and is not
-# particularly robust nor configurable. Run from project parent directory.
-
-# Query GitHub for the username of the given email address.
-# Falls through to the given author name.
-lookup_email_username() {
- lookup=$(curl -G --data-urlencode "q=$1 in:email" https://api.github.com/search/users -H 'Accept: application/vnd.github.v3+json' | grep '"login":' | sed -e 's/^.*": "//g' -e 's/",.*$//g')
-
- if [[ $lookup ]]; then
- echo -n "@$lookup"
- else
- echo "$2"
- fi
-}
-
-# Use formatted log to pull authors list
-authors_raw=$(git log --pretty=format:"%ae|%an" "$(git describe --tags --abbrev=0 @^)"..@)
-readarray -t authors <<<"$authors_raw"
-
-declare -A author_data
-
-for author in "${authors[@]}"; do
- # Match author email
- author_email=${author%|*}
- # Convert to lower case
- author_email=${author_email,,}
- # Match author name
- author_name=${author##*|}
- if [[ -n ${author_data[$author_email]} ]]; then
- # Skip emails we already have data for
- continue
- fi
-
- # Fetch and store author GitHub username by email
- author_data[$author_email]=$(lookup_email_username "$author_email" "$author_name")
-done
-
-# Fetch actual formatted changelog
-changelog=$(git log --pretty=format:"%s (%h) - %ae" "$(git describe --tags --abbrev=0 @^)"..@)
-
-for author_email in "${!author_data[@]}"; do
- # Ignore case when matching
- shopt -s nocasematch
- # Match and replace email
- changelog=${changelog//$author_email/${author_data[$author_email]}}
-done
-
-echo "GENERATED_CHANGELOG<> "$GITHUB_ENV"
diff --git a/scripts/install_spigot_dependencies.sh b/scripts/install_spigot_dependencies.sh
deleted file mode 100644
index 09e93e16..00000000
--- a/scripts/install_spigot_dependencies.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/bin/bash
-#
-# 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 .
-#
-
-# A script for installing required Spigot versions.
-#
-# Note that this script is designed for use in GitHub Actions, and is
-# not particularly robust nor configurable.
-# In its current state, the script must be run from OpenInv's parent
-# project directory and will always install BuildTools to ~/buildtools.
-
-buildtools_dir=~/buildtools
-buildtools=$buildtools_dir/BuildTools.jar
-
-get_spigot_versions () {
- # Get all submodules of internal module
- modules=$(mvn help:evaluate -Dexpression=project.modules -q -DforceStdout -P all -pl internal | grep -oP '(?<=)(.*)(?=<\/string>)')
- for module in "${modules[@]}"; do
-
- # Get number of dependencies declared in pom of specified internal module
- max_index=$(mvn help:evaluate -Dexpression=project.dependencies -q -DforceStdout -P all -pl internal/"$module" | grep -c "")
-
- for ((i=0; i < max_index; i++)); do
- # Get artifactId of dependency
- artifact_id=$(mvn help:evaluate -Dexpression=project.dependencies["$i"].artifactId -q -DforceStdout -P all -pl internal/"$module")
-
- # Ensure dependency is spigot
- if [[ "$artifact_id" == spigot ]]; then
- # Get spigot version
- spigot_version=$(mvn help:evaluate -Dexpression=project.dependencies["$i"].version -q -DforceStdout -P all -pl internal/"$module")
- echo "$spigot_version"
- break
- fi
- done
- done
-}
-
-get_buildtools () {
- if [[ -d $buildtools_dir && -f $buildtools ]]; then
- return
- fi
-
- mkdir $buildtools_dir
- wget https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar -O $buildtools
-}
-
-versions=$(get_spigot_versions)
-echo Found Spigot dependencies: "$versions"
-
-for version in "${versions[@]}"; do
- set -e
- exit_code=0
- mvn dependency:get -Dartifact=org.spigotmc:spigot:"$version" -q -o || exit_code=$?
- if [ $exit_code -ne 0 ]; then
- echo Installing missing Spigot version "$version"
- revision=$(echo "$version" | grep -oP '(\d+\.\d+(\.\d+)?)(?=-R[0-9\.]+-SNAPSHOT)')
- get_buildtools
- java -jar $buildtools -rev "$revision"
- else
- echo Spigot "$version" is already installed
- fi
-done
diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh
new file mode 100755
index 00000000..341d3e2a
--- /dev/null
+++ b/scripts/tag_release.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+#
+# 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 .
+#
+
+if [[ ! $1 ]]; then
+ echo "Please provide a version string."
+ return
+fi
+
+version="$1"
+snapshot="${version%.*}.$((${version##*.} + 1))-SNAPSHOT"
+
+sed -i s/version=.*/version="$version"/ gradle.properties
+
+git add gradle.properties
+git commit -S -m "Bump version to $version for release"
+git tag -s "$version" -m "Release $version"
+
+./gradlew build
+
+sed -i s/version=.*/version="$snapshot"/ gradle.properties
+
+git add gradle.properties
+git commit -S -m "Bump version to $snapshot for development"
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 00000000..efbf5eb0
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,39 @@
+rootProject.name = "openinvparent"
+
+include(":openinvapi")
+project(":openinvapi").projectDir = file("api")
+
+if (!java.lang.Boolean.getBoolean("jitpack")) {
+ val addons = listOf(
+ "togglepersist"
+ )
+ for (addon in addons) {
+ include(":addon$addon")
+ val proj = project(":addon$addon")
+ proj.projectDir = file("addon/$addon")
+ proj.name = "openinv$addon"
+ }
+
+ include(":openinvcommon")
+ project(":openinvcommon").projectDir = file("common")
+
+ val internals = listOf(
+ "common",
+ "paper1_21_10",
+ "paper1_21_8",
+ "paper1_21_5",
+ "paper1_21_4",
+ "paper1_21_3",
+ "paper1_21_1",
+ "spigot"
+ )
+ for (internal in internals) {
+ include(":openinvadapter$internal")
+ project(":openinvadapter$internal").projectDir = file("internal/$internal")
+ }
+
+ include(":resource-pack")
+
+ include(":plugin")
+ project(":plugin").name = "openinvplugin"
+}