Compare commits

..
18 Commits
Author SHA1 Message Date
Jikoo fbdda9637e Bump version to 5.1.0 for release 2024-08-10 12:53:50 -04:00
Jikoo 336aee88e2 Remove reliance on CB conversions
Closes #238
2024-08-10 12:50:46 -04:00
Adam d50b7433c7 Add equal access behavior config (#237) 2024-08-10 15:41:39 +00:00
Jikoo e64261669b More responsibility reduction 2024-08-07 12:54:50 -04:00
Jikoo e8ab490d95 Declare API version for persistence addon 2024-08-07 12:54:16 -04:00
Jikoo 5eaf6243fb Fix modification state being clobbered 2024-08-07 12:54:08 -04:00
Jikoo 5e2cf56d6f Fix "adding" 0 items to last slot
Closes #232
2024-08-07 12:43:06 -04:00
Adam 0f0bde3e49 Move no-snapshot method out of API (#236) 2024-08-07 14:49:37 +00:00
冰砚炽 4198b7f22a Fix white stained glass pane texture (#235) 2024-08-06 09:36:02 -04:00
Newwind 8c15b3d078 Try to get block state without taking snapshots (#234) 2024-08-05 13:25:08 -04:00
dependabot[bot] 335d849525 Bump softprops/action-gh-release from 2.0.6 to 2.0.8 (#231)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.0.6 to 2.0.8.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.0.6...v2.0.8)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-01 15:57:37 -04:00
dependabot[bot] 0fb9e5eed3 Bump org.apache.maven.plugins:maven-jar-plugin from 3.4.1 to 3.4.2 (#229) 2024-08-01 17:36:47 +00:00
dependabot[bot] fe11217a27 Bump com.google.errorprone:error_prone_core from 2.28.0 to 2.29.2 (#230) 2024-08-01 17:36:34 +00:00
Adam 5fea7f3e02 Make toggles transient (#227)
The addon OITogglePersist restores old behavior.
2024-07-28 15:03:44 +00:00
Jikoo dbceff086a Reduce responsibility of main class
This should make the plugin a ton more testable (though I don't have it in me right now to write tests).
2024-07-25 11:57:34 -04:00
Jikoo 64899e5c3d Simplify inventory handling
* Rename view-only inventory for clarity
* Repackage menus and Bukkit implementations for clarity
* Reduce duplicate code somewhat
2024-07-15 14:21:55 -04:00
Adam bca080be7c Restrict view-only further (#226)
* Top inventory now rejects edits entirely
  * This prevents potential issues with plugins ignoring cancelled event status and then doing a strange mix of mismatched queries (like getItem into setContents).
  * It is not feasible/worthwhile to do this with the bottom inventory; many plugins don't use `InventoryView#getBottomInventory`. They use `Player#getInventory` instead.
2024-07-15 10:01:42 -04:00
Jikoo 09842fd168 Bump version to 5.0.1-SNAPSHOT for development 2024-07-12 12:07:12 -04:00
62 changed files with 2465 additions and 1570 deletions

No files matched your search

+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: dist
path: ./target/OpenInv.jar
path: ./target/*.jar
- name: Upload API Jar
id: upload-api
uses: actions/upload-artifact@v4
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v2.0.6
uses: softprops/action-gh-release@v2.0.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.1.0</version>
</parent>
<artifactId>openinvtogglepersist</artifactId>
<dependencies>
<dependency>
<artifactId>annotations</artifactId>
<groupId>org.jetbrains</groupId>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
</dependency>
<dependency>
<artifactId>openinvapi</artifactId>
<groupId>com.lishid</groupId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>OITogglePersist</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-final-jar</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.parent.build.directory}</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}</directory>
<includes>
<include>${project.build.finalName}.jar</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,143 @@
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<UUID, Set<String>> 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<String, List<String>> converted = getSaveData();
YamlConfiguration data = new YamlConfiguration();
for (Map.Entry<String, List<String>> 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<String, List<String>> getSaveData() {
Map<String, List<String>> converted = new HashMap<>();
for (Map.Entry<UUID, Set<String>> 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<String> 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;
});
}
}
}
@@ -0,0 +1,7 @@
name: OITogglePersist
main: com.github.jikoo.openinv.togglepersist.TogglePersist
version: ${project.version}
author: Jikoo
description: An OpenInv addon allowing /anycontainer and /silentcontainer to persist across sessions.
api-version: "1.20"
depend: [ OpenInv ]
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvapi</artifactId>
@@ -3,6 +3,7 @@ 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;
@@ -11,6 +12,8 @@ import org.jetbrains.annotations.NotNull;
*/
public class OpenPlayerSaveEvent extends PlayerSaveEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final ISpecialInventory inventory;
/**
@@ -40,4 +43,14 @@ public class OpenPlayerSaveEvent extends PlayerSaveEvent {
return inventory;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -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.",
link = "",
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;
}
}
@@ -16,15 +16,11 @@
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.Directional;
import org.bukkit.block.data.type.Chest;
import org.bukkit.entity.Cat;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
@@ -57,62 +53,7 @@ public interface IAnySilentContainer {
* @param block the {@link Block} of the container
* @return true if the container is blocked
*/
default boolean isAnyContainerNeeded(@NotNull 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 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);
}
boolean isAnyContainerNeeded(@NotNull Block block);
/**
* Check if a shulker box block cannot be opened under ordinary circumstances.
@@ -123,9 +64,13 @@ public interface IAnySilentContainer {
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);
}
@@ -147,9 +92,7 @@ public interface IAnySilentContainer {
* @param block the potential container
* @return true if the type is a supported container
*/
default boolean isAnySilentContainer(@NotNull Block block) {
return isAnySilentContainer(block.getState());
}
boolean isAnySilentContainer(@NotNull Block block);
/**
* Check if the given {@link BlockState} is a container which can be unblocked or silenced.
@@ -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);
}
@@ -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<String, PlayerToggle> 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<PlayerToggle> 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<UUID> 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);
}
}
}
}
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.lishid</groupId>
<artifactId>openinvparent</artifactId>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvcommon</artifactId>
@@ -1,12 +1,15 @@
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.
*/
@@ -16,8 +19,12 @@ public final class OpenEvents {
return call(new PlayerSaveEvent(player));
}
public static boolean saveCancelled(@NotNull Player player, @NotNull ISpecialInventory inventory) {
return call(new OpenPlayerSaveEvent(player, inventory));
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 <T extends Event & Cancellable> boolean call(T event) {
@@ -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));
}
}
@@ -0,0 +1,7 @@
package com.lishid.openinv.internal;
public interface InternalOwned<T> {
T getOwnerHandle();
}
@@ -1,9 +0,0 @@
package com.lishid.openinv.internal;
import org.bukkit.configuration.ConfigurationSection;
public interface PlaceholderParser {
void load(ConfigurationSection section) throws Exception;
}
@@ -49,6 +49,6 @@ public interface PlayerManager {
*`
* @return the InventoryView opened
*/
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly);
}
@@ -0,0 +1,4 @@
package com.lishid.openinv.internal;
public interface ViewOnly {
}
@@ -274,7 +274,7 @@ public class LanguageManager {
if (sender instanceof Player) {
return ((Player) sender).getLocale();
} else {
return plugin.getConfig().getString("settings.locale", "en_us");
return plugin.getConfig().getString("settings.locale", "en");
}
}
+1 -1
View File
@@ -23,7 +23,7 @@
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvadapter1_20_R3</artifactId>
@@ -16,7 +16,7 @@
package com.lishid.openinv.internal.v1_20_R3;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.AnySilentContainerBase;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
import net.minecraft.core.BlockPos;
@@ -48,7 +48,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
public class AnySilentContainer extends AnySilentContainerBase {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
@@ -105,11 +105,6 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
@@ -244,7 +239,7 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly) {
ServerPlayer nmsPlayer = getHandle(player);
+1 -1
View File
@@ -23,7 +23,7 @@
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvadapter1_20_R4</artifactId>
@@ -16,7 +16,7 @@
package com.lishid.openinv.internal.v1_20_R4;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.AnySilentContainerBase;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
import net.minecraft.core.BlockPos;
@@ -48,7 +48,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
public class AnySilentContainer extends AnySilentContainerBase {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
@@ -105,11 +105,6 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
@@ -244,7 +239,7 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly) {
ServerPlayer nmsPlayer = getHandle(player);
+1 -1
View File
@@ -23,7 +23,7 @@
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvadapter1_21_R1</artifactId>
@@ -16,7 +16,8 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.AnySilentContainerBase;
import com.lishid.openinv.internal.v1_21_R1.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
@@ -49,7 +50,7 @@ import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
public class AnySilentContainer extends AnySilentContainerBase {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
@@ -103,7 +104,7 @@ public class AnySilentContainer implements IAnySilentContainer {
PlayerEnderChestContainer enderChest = player.getEnderChestInventory();
enderChest.setActiveChest(enderChestTile);
player.openMenu(new SimpleMenuProvider((containerCounter, playerInventory, ignored) -> {
MenuType<?> containers = OpenContainerMenu.getContainers(enderChest.getContainerSize());
MenuType<?> containers = OpenChestMenu.getChestMenuType(enderChest.getContainerSize());
int rows = enderChest.getContainerSize() / 9;
return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows);
}, Component.translatable("container.enderchest")));
@@ -1,14 +1,14 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.v1_21_R1.container.menu.OpenEnderChestMenu;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.player.StackedContents;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -25,7 +25,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class OpenEnderChest implements Container, StackedContentsCompatible, MenuProvider, ISpecialEnderChest {
public class OpenEnderChest implements Container, StackedContentsCompatible, InternalOwned<ServerPlayer>,
ISpecialEnderChest {
private CraftInventory inventory;
private @NotNull ServerPlayer owner;
@@ -38,6 +39,11 @@ public class OpenEnderChest implements Container, StackedContentsCompatible, Men
this.items = owner.getEnderChestInventory().items;
}
@Override
public @NotNull ServerPlayer getOwnerHandle() {
return owner;
}
@Override
public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() {
if (inventory == null) {
@@ -174,17 +180,15 @@ public class OpenEnderChest implements Container, StackedContentsCompatible, Men
}
}
@Override
public Component getDisplayName() {
public Component getTitle() {
return Component.translatableWithFallback("openinv.container.enderchest.prefix", "", owner.getName())
.append(Component.translatable("container.enderchest"))
.append(Component.translatableWithFallback("openinv.container.enderchest.suffix", " - %s", owner.getName()));
}
@Override
public @Nullable AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) {
public @Nullable AbstractContainerMenu createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenEnderChestMenu(this, serverPlayer, i);
return new OpenEnderChestMenu(this, serverPlayer, i, viewOnly);
}
return null;
}
@@ -1,127 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotViewOnly;
import com.lishid.openinv.util.Permissions;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventoryView;
class OpenEnderChestMenu extends OpenContainerMenu {
private final OpenEnderChest enderChest;
private final boolean viewOnly;
private final ServerPlayer viewer;
private final int topSize;
private CraftInventoryView view;
OpenEnderChestMenu(OpenEnderChest enderChest, ServerPlayer viewer, int containerId) {
super(getMenuType(enderChest), containerId);
this.enderChest = enderChest;
this.viewer = viewer;
var bukkitViewer = viewer.getBukkitEntity();
viewOnly = !(bukkitViewer.equals(enderChest.getPlayer())
? Permissions.ENDERCHEST_EDIT_SELF
: Permissions.ENDERCHEST_OPEN_OTHER)
.hasPermission(bukkitViewer);
int upperRows = (int) Math.ceil(enderChest.getContainerSize() / 9.0);
topSize = upperRows * 9;
// View's upper inventory - our container
for (int row = 0; row < upperRows; ++row) {
for (int col = 0; col < 9; ++col) {
// x and y for client purposes, but hey, we're thorough here.
// Adapted from net.minecraft.world.inventory.ChestMenu
int x = 8 + col * 18;
int y = 18 + row * 18;
int index = row * 9 + col;
// Guard against weird inventory sizes.
if (index >= enderChest.getContainerSize()) {
addSlot(new SlotViewOnly(enderChest, index, x, y));
continue;
}
addSlot(new Slot(enderChest, index, x, y));
}
}
// View's lower inventory - viewer inventory
int playerInvPad = (upperRows - 4) * 18;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + row * 18 + 103;
addSlot(new Slot(viewer.getInventory(), row * 9 + col + 9, x, y));
}
}
// Hotbar
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + 161;
addSlot(new Slot(viewer.getInventory(), col, x, y));
}
}
private static MenuType<?> getMenuType(OpenEnderChest enderChest) {
return OpenContainerMenu.getContainers(((int) Math.ceil(enderChest.getContainerSize() / 9.0)) * 9);
}
@Override
public CraftInventoryView getBukkitView() {
if (view == null) {
view = new CraftInventoryView(viewer.getBukkitEntity(), enderChest.getBukkitInventory(), this);
}
return view;
}
@Override
protected Slot addSlot(Slot slot) {
slot = super.addSlot(slot);
// If view-only and slot is in upper inventory, wrap it.
if (viewOnly && slot.index < enderChest.getContainerSize()) {
slot = SlotViewOnly.wrap(slot);
slots.set(slot.index, slot);
}
return slot;
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
// 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;
}
@Override
public boolean stillValid(Player entityhuman) {
return true;
}
}
@@ -1,7 +1,9 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.v1_21_R1.container.bukkit.OpenPlayerInventory;
import com.lishid.openinv.internal.v1_21_R1.container.menu.OpenInventoryMenu;
import com.lishid.openinv.internal.v1_21_R1.container.slot.Content;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentCrafting;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentCraftingResult;
@@ -12,6 +14,7 @@ import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentList;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentOffHand;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentViewOnly;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotViewOnly;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import net.minecraft.ChatFormatting;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
@@ -19,7 +22,6 @@ import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
@@ -37,7 +39,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class OpenInventory implements Container, MenuProvider, ISpecialPlayerInventory {
public class OpenInventory implements Container, InternalOwned<ServerPlayer>, ISpecialPlayerInventory {
private final List<Content> slots;
private final int size;
@@ -203,10 +205,6 @@ public class OpenInventory implements Container, MenuProvider, ISpecialPlayerInv
return slots.get(index).getSlotType();
}
public ServerPlayer getOwnerHandle() {
return owner;
}
public @NotNull Component getTitle(@Nullable ServerPlayer viewer) {
MutableComponent component = Component.empty();
// Prefix for use with custom bitmap image fonts.
@@ -230,6 +228,11 @@ public class OpenInventory implements Container, MenuProvider, ISpecialPlayerInv
return component;
}
@Override
public ServerPlayer getOwnerHandle() {
return owner;
}
@Override
public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() {
if (bukkitEntity == null) {
@@ -351,15 +354,9 @@ public class OpenInventory implements Container, MenuProvider, ISpecialPlayerInv
owner.containerMenu.setCarried(ItemStack.EMPTY);
}
@Override
public Component getDisplayName() {
return getTitle(null);
}
@Override
public @Nullable AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) {
public @Nullable AbstractContainerMenu createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenInventoryMenu(this, serverPlayer, i);
return new OpenInventoryMenu(this, serverPlayer, i, viewOnly);
}
return null;
}
@@ -1,295 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentDrop;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentEquipment;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotViewOnly;
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.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventoryView;
import org.bukkit.craftbukkit.v1_21_R1.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 OpenContainerMenu {
private final OpenInventory inventory;
private final ServerPlayer viewer;
private final int topSize;
private final int offset;
private final boolean ownInv;
private CraftInventoryView bukkitEntity;
protected OpenInventoryMenu(OpenInventory inventory, ServerPlayer viewer, int i) {
super(getMenuType(inventory, viewer), i);
this.inventory = inventory;
this.viewer = viewer;
ownInv = inventory.getOwnerHandle().equals(viewer);
int upperRows;
if (ownInv) {
// Disallow duplicate access to own main inventory contents.
offset = viewer.getInventory().items.size();
upperRows = ((int) Math.ceil((inventory.getContainerSize() - offset) / 9.0));
} else {
offset = 0;
upperRows = inventory.getContainerSize() / 9;
}
boolean viewOnly = !(ownInv ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER)
.hasPermission(viewer.getBukkitEntity());
// View's upper inventory - our container
for (int row = 0; row < upperRows; ++row) {
for (int col = 0; col < 9; ++col) {
// x and y for client purposes, but hey, we're thorough here.
// Adapted from net.minecraft.world.inventory.ChestMenu
int x = 8 + col * 18;
int y = 18 + row * 18;
int index = offset + row * 9 + col;
// Guard against weird inventory sizes.
if (index >= inventory.getContainerSize()) {
addSlot(new SlotViewOnly(inventory, index, x, y));
continue;
}
Slot slot = getUpperSlot(index, x, y, ownInv, viewOnly);
addSlot(slot);
}
}
// View's lower inventory - viewer inventory
int playerInvPad = (upperRows - 4) * 18;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + row * 18 + 103;
addSlot(new Slot(viewer.getInventory(), row * 9 + col + 9, x, y));
}
}
// Hotbar
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + 161;
addSlot(new Slot(viewer.getInventory(), col, x, y));
}
this.topSize = slots.size() - 36;
}
private Slot getUpperSlot(int index, int x, int y, boolean ownInv, boolean viewOnly) {
Slot slot = inventory.getMenuSlot(index, x, y);
// If the slot is 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(inventory, 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(inventory.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 (ownInv && !(slot instanceof ContentDrop.SlotDrop)) {
return new SlotViewOnly(inventory, index, x, y);
}
if (viewOnly) {
return SlotViewOnly.wrap(slot);
}
return slot;
}
private static MenuType<?> getMenuType(OpenInventory inventory, ServerPlayer viewer) {
int size = inventory.getContainerSize();
if (inventory.getOwnerHandle().equals(viewer)) {
size -= viewer.getInventory().items.size();
size = ((int) Math.ceil(size / 9.0)) * 9;
}
return OpenContainerMenu.getContainers(size);
}
@Override
public CraftInventoryView getBukkitView() {
if (bukkitEntity == null) {
org.bukkit.inventory.Inventory bukkitInventory;
if (ownInv) {
bukkitInventory = new OpenPlayerInventorySelf(inventory, offset);
} else {
bukkitInventory = inventory.getBukkitInventory();
}
bukkitEntity = new CraftInventoryView(viewer.getBukkitEntity(), bukkitInventory, this) {
@Override
public org.bukkit.inventory.ItemStack getItem(int index) {
if (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 (rawSlot < 0) {
return super.getInventory(rawSlot);
}
if (rawSlot > topSize) {
return super.getInventory(offset + rawSlot);
}
Slot slot = slots.get(rawSlot);
if (slot.isFake()) {
return null;
}
return getTopInventory();
}
@Override
public int convertSlot(int rawSlot) {
if (rawSlot < 0) {
return rawSlot;
}
if (rawSlot < topSize) {
Slot slot = slots.get(rawSlot);
if (slot.isFake()) {
return InventoryView.OUTSIDE;
}
return rawSlot;
}
return super.convertSlot(offset + rawSlot);
}
@Override
public @NotNull InventoryType.SlotType getSlotType(int slot) {
if (slot < 0) {
return InventoryType.SlotType.OUTSIDE;
}
if (slot >= topSize) {
return super.getSlotType(offset + slot);
}
return inventory.getSlotType(offset + slot);
}
@Override
public int countSlots() {
return topSize + getBottomInventory().getSize();
}
};
}
return bukkitEntity;
}
@Override
public ItemStack quickMoveStack(Player player, int index) {
// 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 = inventory.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, inventory.getOwnerHandle().getInventory().items.size(), true)) {
return ItemStack.EMPTY;
}
}
}
if (itemStack.isEmpty()) {
slot.setByPlayer(ItemStack.EMPTY);
} else {
slot.setChanged();
}
return originalStack;
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public boolean canDragTo(Slot slot) {
return !(slot instanceof ContentDrop.SlotDrop || slot instanceof SlotViewOnly);
}
}
@@ -0,0 +1,163 @@
package com.lishid.openinv.internal.v1_21_R1.container.bukkit;
import com.lishid.openinv.internal.ViewOnly;
import net.minecraft.world.Container;
import org.bukkit.Material;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.ListIterator;
/**
* A locked down "empty" inventory that rejects plugin interaction.
*/
public class OpenDummyInventory extends CraftInventory implements ViewOnly {
public OpenDummyInventory(Container inventory) {
super(inventory);
}
@Override
public @Nullable ItemStack getItem(int index) {
return null;
}
@Override
public void setItem(int index, @Nullable ItemStack item) {
}
@SuppressWarnings("NonApiType")
@Override
public @NotNull HashMap<Integer, ItemStack> addItem(@NotNull ItemStack... items) throws IllegalArgumentException {
return arrayToHashMap(items);
}
@SuppressWarnings("NonApiType")
@Override
public @NotNull HashMap<Integer, ItemStack> removeItem(@NotNull ItemStack... items) throws IllegalArgumentException {
return arrayToHashMap(items);
}
@SuppressWarnings("NonApiType")
private static @NotNull HashMap<Integer, ItemStack> arrayToHashMap(@NotNull ItemStack[] items) {
HashMap<Integer, ItemStack> ignored = new HashMap<>();
for (int index = 0; index < items.length; ++index) {
ignored.put(index, items[index]);
}
return ignored;
}
@Override
public ItemStack[] getContents() {
return new ItemStack[getSize()];
}
@Override
public void setContents(@NotNull ItemStack[] items) throws IllegalArgumentException {
}
@Override
public @NotNull ItemStack[] getStorageContents() {
return new ItemStack[getSize()];
}
@Override
public void setStorageContents(@NotNull ItemStack[] items) throws IllegalArgumentException {
}
@Override
public boolean contains(@NotNull Material material) throws IllegalArgumentException {
return false;
}
@Override
public boolean contains(@Nullable ItemStack item) {
return false;
}
@Override
public boolean contains(@NotNull Material material, int amount) throws IllegalArgumentException {
return false;
}
@Override
public boolean contains(@Nullable ItemStack item, int amount) {
return false;
}
@Override
public boolean containsAtLeast(@Nullable ItemStack item, int amount) {
return false;
}
@SuppressWarnings("NonApiType")
@Override
public @NotNull HashMap<Integer, ItemStack> all(
@NotNull Material material) throws IllegalArgumentException {
return new HashMap<>();
}
@SuppressWarnings("NonApiType")
@Override
public @NotNull HashMap<Integer, ItemStack> all(@Nullable ItemStack item) {
return new HashMap<>();
}
@Override
public int first(@NotNull Material material) throws IllegalArgumentException {
return -1;
}
@Override
public int first(@NotNull ItemStack item) {
return -1;
}
@Override
public int firstEmpty() {
return -1;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public void remove(@NotNull Material material) throws IllegalArgumentException {
}
@Override
public void remove(@NotNull ItemStack item) {
}
@Override
public void clear(int index) {
}
@Override
public void clear() {
}
@Override
public @NotNull ListIterator<ItemStack> iterator() {
return Collections.emptyListIterator();
}
@Override
public @NotNull ListIterator<ItemStack> iterator(int index) {
return Collections.emptyListIterator();
}
}
@@ -1,6 +1,7 @@
package com.lishid.openinv.internal.v1_21_R1.container;
package com.lishid.openinv.internal.v1_21_R1.container.bukkit;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.v1_21_R1.container.OpenInventory;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
@@ -20,7 +21,7 @@ public class OpenPlayerInventory extends CraftInventory implements PlayerInvento
}
@Override
public OpenInventory getInventory() {
public @NotNull OpenInventory getInventory() {
return (OpenInventory) super.getInventory();
}
@@ -1,5 +1,6 @@
package com.lishid.openinv.internal.v1_21_R1.container;
package com.lishid.openinv.internal.v1_21_R1.container.bukkit;
import com.lishid.openinv.internal.v1_21_R1.container.OpenInventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
@@ -1,10 +1,19 @@
package com.lishid.openinv.internal.v1_21_R1.container;
package com.lishid.openinv.internal.v1_21_R1.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.v1_21_R1.container.bukkit.OpenDummyInventory;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotPlaceholder;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotViewOnly;
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.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
@@ -12,6 +21,10 @@ 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.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventoryView;
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;
@@ -22,8 +35,18 @@ import java.util.function.Supplier;
/**
* An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}.
*/
public abstract class OpenContainerMenu extends AbstractContainerMenu {
public abstract class OpenChestMenu<T extends Container & ISpecialInventory & InternalOwned<ServerPlayer>>
extends AbstractContainerMenu {
protected static final int BOTTOM_INVENTORY_SIZE = 36;
protected final T container;
protected final ServerPlayer viewer;
protected final boolean viewOnly;
protected final boolean ownContainer;
protected final int topSize;
private CraftInventoryView<OpenChestMenu<T>> bukkitEntity;
// Syncher fields
private @Nullable ContainerSynchronizer synchronizer;
private final List<DataSlot> dataSlots = new ArrayList<>();
private final IntList remoteDataSlots = new IntArrayList();
@@ -31,20 +54,145 @@ public abstract class OpenContainerMenu extends AbstractContainerMenu {
private ItemStack remoteCarried = ItemStack.EMPTY;
private boolean suppressRemoteUpdates;
protected OpenContainerMenu(@Nullable MenuType<?> containers, int containerCounter) {
super(containers, containerCounter);
protected OpenChestMenu(
@NotNull MenuType<ChestMenu> type,
int containerCounter,
@NotNull T container,
@NotNull ServerPlayer viewer,
boolean viewOnly) {
super(type, containerCounter);
this.container = container;
this.viewer = viewer;
this.viewOnly = viewOnly;
ownContainer = container.getOwnerHandle().equals(viewer);
topSize = getTopSize(viewer);
preSlotSetup();
int upperRows = topSize / 9;
// View's upper inventory - our container
for (int row = 0; row < upperRows; ++row) {
for (int col = 0; col < 9; ++col) {
// x and y for client purposes, but hey, we're thorough here.
// Adapted from net.minecraft.world.inventory.ChestMenu
int x = 8 + col * 18;
int y = 18 + row * 18;
int index = row * 9 + col;
// Guard against weird inventory sizes.
if (index >= container.getContainerSize()) {
addSlot(new SlotViewOnly(container, index, x, y));
continue;
}
Slot slot = getUpperSlot(index, x, y);
addSlot(slot);
}
}
// View's lower inventory - viewer inventory
int playerInvPad = (upperRows - 4) * 18;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + row * 18 + 103;
addSlot(new Slot(viewer.getInventory(), row * 9 + col + 9, x, y));
}
}
// Hotbar
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + 161;
addSlot(new Slot(viewer.getInventory(), col, x, y));
}
}
static @NotNull MenuType<?> getContainers(int inventorySize) {
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 36 -> MenuType.GENERIC_9x4;
case 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
// Default to 27-slot inventory
default -> MenuType.GENERIC_9x3;
};
public static @NotNull MenuType<ChestMenu> getChestMenuType(int inventorySize) {
inventorySize = ((int) Math.ceil(inventorySize / 9.0)) * 9;
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 27 -> MenuType.GENERIC_9x3;
case 36 -> MenuType.GENERIC_9x4;
case 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
default -> throw new IllegalArgumentException("Inventory size unsupported: " + inventorySize);
};
}
protected void preSlotSetup() {}
protected @NotNull Slot getUpperSlot(int index, int x, int y) {
if (viewOnly) {
return new SlotViewOnly(container, index, x, y);
}
return new Slot(container, index, x, y);
}
@Override
public final @NotNull CraftInventoryView<OpenChestMenu<T>> getBukkitView() {
if (bukkitEntity == null) {
bukkitEntity = createBukkitEntity();
}
return bukkitEntity;
}
protected @NotNull CraftInventoryView<OpenChestMenu<T>> createBukkitEntity() {
Inventory top;
if (viewOnly) {
top = new OpenDummyInventory(container);
} else {
top = container.getBukkitInventory();
}
return new CraftInventoryView<>(viewer.getBukkitEntity(), top, this) {
@Override
public @Nullable Inventory getInventory(int rawSlot) {
if (viewOnly) {
return null;
}
return super.getInventory(rawSlot);
}
@Override
public int convertSlot(int rawSlot) {
if (viewOnly) {
return InventoryView.OUTSIDE;
}
return super.convertSlot(rawSlot);
}
@Override
public @NotNull InventoryType.SlotType getSlotType(int slot) {
if (viewOnly) {
return InventoryType.SlotType.OUTSIDE;
}
return super.getSlotType(slot);
}
};
}
private int getTopSize(ServerPlayer viewer) {
MenuType<?> menuType = getType();
if (menuType == null) {
throw new IllegalStateException("MenuType cannot be null!");
} else if (menuType == MenuType.GENERIC_9x1) {
return 9;
} else if (menuType == MenuType.GENERIC_9x2) {
return 18;
} else if (menuType == MenuType.GENERIC_9x3) {
return 27;
} else if (menuType == MenuType.GENERIC_9x4) {
return 36;
} else if (menuType == MenuType.GENERIC_9x5) {
return 45;
} else if (menuType == MenuType.GENERIC_9x6) {
return 54;
}
// This is a bit gross, but allows us a safe fallthrough.
return menuType.create(-1, viewer.getInventory()).slots.size() - BOTTOM_INVENTORY_SIZE;
}
/**
@@ -79,7 +227,7 @@ public abstract class OpenContainerMenu extends AbstractContainerMenu {
continue;
}
// Otherwise, add as many as we can from our stack to the slot.
modified = addToExistingStack(itemStack, slot);
modified |= addToExistingStack(itemStack, slot);
} else {
// If this is the first empty slot, keep track of it for later use.
if (firstEmpty == null) {
@@ -110,8 +258,15 @@ public abstract class OpenContainerMenu extends AbstractContainerMenu {
return false;
}
int total = existing.getCount() + itemStack.getCount();
int max = slot.getMaxStackSize(existing);
int existingCount = existing.getCount();
// If the stack is already full, we can't add more.
if (existingCount >= max) {
return false;
}
int total = existingCount + itemStack.getCount();
// If the existing item can accept the entirety of our item, we're done!
if (total <= max) {
@@ -122,12 +277,28 @@ public abstract class OpenContainerMenu extends AbstractContainerMenu {
}
// Otherwise, add as many as we can.
itemStack.shrink(max - existing.getCount());
itemStack.shrink(max - existingCount);
existing.setCount(max);
slot.setChanged();
return true;
}
@Override
public void clicked(int i, int j, ClickType clickType, Player player) {
if (viewOnly) {
if (clickType == ClickType.QUICK_CRAFT) {
sendAllDataToRemote();
}
return;
}
super.clicked(i, j, clickType, player);
}
@Override
public boolean stillValid(Player player) {
return true;
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected Slot addSlot(Slot slot) {
@@ -0,0 +1,53 @@
package com.lishid.openinv.internal.v1_21_R1.container.menu;
import com.lishid.openinv.internal.v1_21_R1.container.OpenEnderChest;
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 OpenChestMenu<OpenEnderChest> {
public OpenEnderChestMenu(
@NotNull OpenEnderChest enderChest,
@NotNull ServerPlayer viewer,
int containerId,
boolean viewOnly) {
super(getChestMenuType(enderChest.getContainerSize()), containerId, enderChest, viewer, viewOnly);
}
@Override
public ItemStack quickMoveStack(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;
}
}
@@ -0,0 +1,262 @@
package com.lishid.openinv.internal.v1_21_R1.container.menu;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.v1_21_R1.container.OpenInventory;
import com.lishid.openinv.internal.v1_21_R1.container.bukkit.OpenDummyInventory;
import com.lishid.openinv.internal.v1_21_R1.container.bukkit.OpenPlayerInventorySelf;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentDrop;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentEquipment;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotViewOnly;
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.v1_21_R1.inventory.CraftInventoryView;
import org.bukkit.craftbukkit.v1_21_R1.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 OpenChestMenu<OpenInventory> {
private int offset;
public OpenInventoryMenu(OpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) {
super(getMenuType(inventory, viewer), i, inventory, viewer, viewOnly);
}
private static MenuType<ChestMenu> 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 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<OpenChestMenu<OpenInventory>> createBukkitEntity() {
org.bukkit.inventory.Inventory bukkitInventory;
if (viewOnly) {
bukkitInventory = new OpenDummyInventory(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 ItemStack quickMoveStack(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;
}
}
@@ -13,7 +13,6 @@ 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.MenuProvider;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.ChatVisiblity;
import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -85,11 +84,6 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
@@ -224,35 +218,34 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory) {
public @Nullable InventoryView openInventory(@NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory, boolean viewOnly) {
ServerPlayer player = getHandle(bukkitPlayer);
if (player.connection == null) {
return null;
}
MenuProvider provider;
// See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
AbstractContainerMenu menu;
Component title;
if (inventory instanceof OpenInventory playerInv) {
provider = playerInv;
menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly);
title = playerInv.getTitle(player);
} else if (inventory instanceof OpenEnderChest enderChest) {
provider = enderChest;
title = enderChest.getDisplayName();
menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly);
title = enderChest.getTitle();
} else {
return null;
}
// See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
AbstractContainerMenu menu = provider.createMenu(player.nextContainerCounter(), player.getInventory(), player);
// Should never happen, player is a ServerPlayer with an active connection.
if (menu == null) {
return null;
}
// Set up title (the whole reason we're not just using Player#openInventory(Inventory)).
// Title can only be set once for a menu, and is set during the open process.
// 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);
menu = CraftEventFactory.callInventoryOpenEvent(player, menu, false);
+4 -4
View File
@@ -21,7 +21,7 @@
<parent>
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<version>5.0.0</version>
<version>5.1.0</version>
</parent>
<artifactId>openinvplugin</artifactId>
@@ -47,19 +47,19 @@
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_21_R1</artifactId>
<version>5.0.0</version>
<version>5.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_20_R4</artifactId>
<version>5.0.0</version>
<version>5.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_20_R3</artifactId>
<version>5.0.0</version>
<version>5.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
@@ -1,134 +0,0 @@
/*
* 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
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv;
import com.google.errorprone.annotations.Keep;
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.Permissions;
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.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.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
/**
* Listener for inventory-related events to prevent modification of inventories where not allowed.
*
* @author Jikoo
*/
record InventoryListener(OpenInv plugin) implements Listener {
@Keep
@EventHandler
private void onInventoryClose(@NotNull final InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
InventoryHolder holder = event.getInventory().getHolder();
if (this.plugin.getSilentContainerStatus(player)
&& holder != null
&& this.plugin.getAnySilentContainer().isAnySilentContainer(holder)) {
this.plugin.getAnySilentContainer().deactivateContainer(player);
}
ISpecialInventory specialInventory = InventoryAccess.getEnderChest(event.getInventory());
if (specialInventory != null) {
this.plugin.handleCloseInventory(specialInventory);
} else {
specialInventory = InventoryAccess.getPlayerInventory(event.getInventory());
if (specialInventory != null) {
this.plugin.handleCloseInventory(specialInventory);
}
}
}
@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);
}
/**
* Handle common InventoryInteractEvent functions.
*
* @param event the InventoryInteractEvent
*/
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();
ISpecialInventory backing = InventoryAccess.getInventory(inventory);
// Not a special inventory.
if (backing == null) {
return;
}
Permissions editSelf;
Permissions editOther;
if (backing instanceof ISpecialEnderChest) {
editSelf = Permissions.ENDERCHEST_EDIT_SELF;
editOther = Permissions.ENDERCHEST_EDIT_OTHER;
} else if (backing instanceof ISpecialPlayerInventory) {
editSelf = Permissions.INVENTORY_EDIT_SELF;
editOther = Permissions.INVENTORY_EDIT_OTHER;
} else {
// Unknown implementation.
return;
}
if (Objects.equals(entity, backing.getPlayer())) {
if (!editSelf.hasPermission(entity)) {
event.setCancelled(true);
}
} else {
if (!editOther.hasPermission(entity)) {
event.setCancelled(true);
}
}
}
}
@@ -1,42 +0,0 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.util.Permissions;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Consumer;
record OfflineHandler(
@NotNull BiFunction<Map<UUID, ? extends ISpecialInventory>, UUID, ISpecialInventory> fetch,
@NotNull Consumer<@NotNull ISpecialInventory> handle) {
static final OfflineHandler REMOVE_AND_CLOSE = new OfflineHandler(
Map::remove,
inventory -> OpenInv.ejectViewers(inventory, viewer -> true)
);
static final OfflineHandler REQUIRE_PERMISSIONS = new OfflineHandler(
Map::get,
inventory -> OpenInv.ejectViewers(inventory, viewer -> !Permissions.ACCESS_OFFLINE.hasPermission(viewer))
);
}
@@ -18,25 +18,29 @@ package com.lishid.openinv;
import com.github.jikoo.planarwrappers.util.version.BukkitVersions;
import com.github.jikoo.planarwrappers.util.version.Version;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
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.event.OpenEvents;
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.util.ConfigUpdater;
import com.lishid.openinv.listener.ContainerListener;
import com.lishid.openinv.listener.LegacyInventoryListener;
import com.lishid.openinv.listener.ToggleListener;
import com.lishid.openinv.util.AccessEqualMode;
import com.lishid.openinv.util.InternalAccessor;
import com.lishid.openinv.util.LangMigrator;
import com.lishid.openinv.util.InventoryManager;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.StringMetric;
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 org.bukkit.Bukkit;
import com.lishid.openinv.util.setting.PlayerToggle;
import com.lishid.openinv.util.setting.PlayerToggles;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@@ -44,51 +48,36 @@ import org.bukkit.command.CommandSender;
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.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.profile.PlayerProfile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.stream.Stream;
/**
* Open other player's inventory
*
* @author lishid
* The main class for OpenInv.
*/
public class OpenInv extends JavaPlugin implements IOpenInv {
private final Cache<String, PlayerProfile> offlineLookUpCache = CacheBuilder.newBuilder().maximumSize(10).build();
private final Map<UUID, ISpecialPlayerInventory> inventories = new ConcurrentHashMap<>();
private final Map<UUID, ISpecialEnderChest> enderChests = new ConcurrentHashMap<>();
private InternalAccessor accessor;
private Config config;
private InventoryManager inventoryManager;
private LanguageManager languageManager;
private PlayerLoader playerLoader;
private boolean isSpigot = false;
private OfflineHandler offlineHandler;
@Override
public void reloadConfig() {
super.reloadConfig();
config.reload(getConfig());
languageManager.reload();
this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS;
if (this.accessor != null && this.accessor.isSupported()) {
this.accessor.reload(this.getConfig());
if (accessor != null && accessor.isSupported()) {
accessor.reload(getConfig());
}
}
@@ -103,27 +92,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
@Override
public void onDisable() {
if (this.disableSaving()) {
return;
}
Stream.concat(inventories.values().stream(), enderChests.values().stream())
.map(inventory -> {
// Cheat a bit - rather than stream twice, evict all viewers during remapping.
ejectViewers(inventory, viewer -> true);
if (inventory.getPlayer() instanceof Player player) {
return player;
}
return null;
})
.filter(Objects::nonNull)
.distinct()
.forEach(player -> {
if (!player.isOnline()) {
player = accessor.getPlayerDataManager().inject(player);
}
player.saveData();
});
inventoryManager.evictAll();
}
@Override
@@ -135,8 +104,17 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
Path dataFolder = getDataFolder().toPath();
new LangMigrator(dataFolder, dataFolder.resolve("locale"), getLogger()).migrate();
this.languageManager = new LanguageManager(this, "en");
this.accessor = new InternalAccessor(getLogger(), languageManager);
// 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");
@@ -152,22 +130,11 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
// Update existing configuration. May require internal access.
new ConfigUpdater(this).checkForUpdates();
// Get plugin manager
PluginManager pm = this.getServer().getPluginManager();
// Register listeners
if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 21))) {
pm.registerEvents(new LegacyInventoryListener(this), this);
} else {
pm.registerEvents(new PlayerListener(this, languageManager), this);
}
pm.registerEvents(new InventoryListener(this), this);
// Register relevant event listeners.
registerEvents();
// Register commands to their executors
this.setCommandExecutor(new OpenInvCommand(this, languageManager), "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 ContainerSettingCommand(this, languageManager), "silentcontainer", "anycontainer");
// Register commands to their executors.
registerCommands();
} else {
this.sendVersionError(this.getLogger()::warning);
@@ -175,6 +142,30 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
}
private void registerEvents() {
PluginManager pluginManager = this.getServer().getPluginManager();
// Legacy: extra listener for permission handling and self-view issue prevention.
if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 21))) {
pluginManager.registerEvents(new LegacyInventoryListener(this, config), this);
}
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");
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);
@@ -210,17 +201,17 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
@Override
public boolean disableSaving() {
return this.getConfig().getBoolean("settings.disable-saving", false);
return config.isSaveDisabled();
}
@Override
public boolean disableOfflineAccess() {
return this.getConfig().getBoolean("settings.disable-offline-access", false);
return config.isOfflineDisabled();
}
@Override
public boolean noArgsOpensSelf() {
return this.getConfig().getBoolean("settings.command.open.no-args-opens-self", false);
return config.doesNoArgsOpenSelf();
}
@Override
@@ -230,379 +221,94 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
@Override
public boolean getAnyContainerStatus(@NotNull final OfflinePlayer offline) {
return this.getConfig().getBoolean("toggles.any-chest." + offline.getUniqueId(), false);
return PlayerToggles.any().is(offline.getUniqueId());
}
@Override
public void setAnyContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.any-chest." + offline.getUniqueId(), status);
this.saveConfig();
PlayerToggles.any().set(offline.getUniqueId(), status);
}
@Override
public boolean getSilentContainerStatus(@NotNull final OfflinePlayer offline) {
return this.getConfig().getBoolean("toggles.silent-chest." + offline.getUniqueId(), false);
return PlayerToggles.silent().is(offline.getUniqueId());
}
@Override
public void setSilentContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.silent-chest." + offline.getUniqueId(), status);
this.saveConfig();
PlayerToggles.silent().set(offline.getUniqueId(), status);
}
@Override
public @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) {
UUID key = player.getUniqueId();
if (this.enderChests.containsKey(key)) {
return this.enderChests.get(key);
}
ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player);
this.enderChests.put(key, inv);
return inv;
return inventoryManager.getEnderChest(player);
}
@Override
public @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) {
UUID key = player.getUniqueId();
if (this.inventories.containsKey(key)) {
return this.inventories.get(key);
}
ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player);
this.inventories.put(key, inv);
return inv;
return inventoryManager.getInventory(player);
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
return this.accessor.getPlayerDataManager().openInventory(player, 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_OPEN_OTHER;
}
boolean viewOnly = edit != null && !edit.hasPermission(player);
if (!ownContainer && !viewOnly) {
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 player doesn't have an equal access level or equal access is a denial, deny.
if (!player.hasPermission(permission) || config.getAccessEqualMode() == AccessEqualMode.DENY) {
return null;
}
// Since this is a tie, setting decides view state.
if (config.getAccessEqualMode() == AccessEqualMode.VIEW) {
viewOnly = true;
}
break;
}
}
return this.accessor.openInventory(player, inventory, viewOnly);
}
@Override
public boolean isPlayerLoaded(@NotNull UUID playerUuid) {
return this.inventories.containsKey(playerUuid) || this.enderChests.containsKey(playerUuid);
return inventoryManager.getLoadedPlayer(playerUuid) != null;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
UUID key = offline.getUniqueId();
if (this.inventories.containsKey(key)) {
return (Player) this.inventories.get(key).getPlayer();
}
if (this.enderChests.containsKey(key)) {
return (Player) this.enderChests.get(key).getPlayer();
}
Player player = offline.getPlayer();
if (player != null) {
return player;
}
if (disableOfflineAccess() || !this.isSupportedVersion()) {
return null;
}
if (Bukkit.isPrimaryThread()) {
return this.accessor.getPlayerDataManager().loadPlayer(offline);
}
Future<Player> future = Bukkit.getScheduler().callSyncMethod(this,
() -> OpenInv.this.accessor.getPlayerDataManager().loadPlayer(offline));
try {
player = future.get();
} catch (InterruptedException | ExecutionException e) {
getLogger().log(Level.WARNING, e.getMessage(), e);
return null;
}
return player;
return playerLoader.load(offline);
}
@Override
public @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().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 = offlineLookUpCache.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()) {
offlineLookUpCache.put(name, player.getPlayerProfile());
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.
offlineLookUpCache.put(name, player.getPlayerProfile());
return player;
}
// No players have ever joined the server.
return null;
return playerLoader.match(name);
}
@Override
public void unload(@NotNull final OfflinePlayer offline) {
setPlayerOffline(offline, OfflineHandler.REMOVE_AND_CLOSE);
}
/**
* Evict all viewers lacking cross-world permissions when a {@link Player} changes worlds.
*
* @param player the Player
*/
void changeWorld(@NotNull Player player) {
UUID key = player.getUniqueId();
if (this.inventories.containsKey(key)) {
kickCrossWorldViewers(player, this.inventories.get(key));
}
if (this.enderChests.containsKey(key)) {
kickCrossWorldViewers(player, this.enderChests.get(key));
}
}
private void kickCrossWorldViewers(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ejectViewers(
inventory,
viewer ->
!Permissions.ACCESS_CROSSWORLD.hasPermission(viewer)
&& !Objects.equals(viewer.getWorld(), player.getWorld()));
}
/**
* Method for handling a Player going offline.
*
* @param player the Player
*/
void setPlayerOffline(@NotNull Player player) {
setPlayerOffline(player, offlineHandler);
}
private void setPlayerOffline(@NotNull OfflinePlayer player, @NotNull OfflineHandler handler) {
UUID key = player.getUniqueId();
setPlayerOffline(inventories, key, handler);
setPlayerOffline(enderChests, key, handler);
}
private void setPlayerOffline(
@NotNull Map<UUID, ? extends ISpecialInventory> map,
@NotNull UUID key,
@NotNull OfflineHandler handler) {
ISpecialInventory inventory = handler.fetch().apply(map, key);
if (inventory == null) {
return;
}
inventory.setPlayerOffline();
if (!inventory.isInUse()) {
map.remove(key);
} else {
handler.handle().accept(inventory);
}
}
void handleCloseInventory(@NotNull ISpecialInventory inventory) {
Map<UUID, ? extends ISpecialInventory> map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests;
UUID key = inventory.getPlayer().getUniqueId();
@Nullable ISpecialInventory loaded = map.get(key);
if (loaded == null) {
// Loaded inventory has already been removed. Removal will handle saving if necessary.
return;
}
// This should only be possible if a plugin is doing funky things with our inventories.
if (loaded != inventory) {
Inventory bukkitInventory = inventory.getBukkitInventory();
// Just in case, respect contents of the inventory that was just used.
loaded.getBukkitInventory().setContents(bukkitInventory.getContents());
// We need to close this inventory to reduce risk of duplication bugs if the user is offline.
// We don't want to risk recursively closing the same inventory repeatedly, so we schedule dumping viewers.
// Worst case we schedule a couple redundant tasks if several people had the inventory open.
if (inventory.isInUse()) {
getServer().getScheduler().runTask(this, () -> ejectViewers(inventory, viewer -> true));
}
}
// Schedule task to check in use status later this tick. Closing user is still in viewer list.
getServer().getScheduler().runTask(this, () -> {
if (loaded.isInUse()) {
return;
}
// Re-fetch from map - prevents duplicate saves on multi-close.
ISpecialInventory current = map.remove(key);
if (disableSaving()
|| current == null
|| !(current.getPlayer() instanceof Player player)
|| player.isOnline()) {
return;
}
if (!OpenEvents.saveCancelled(player, current)) {
this.accessor.getPlayerDataManager().inject(player).saveData();
}
});
}
/**
* Method for handling a Player coming online.
*
* @param player the Player
* @throws IllegalStateException if the server version is unsupported
*/
void setPlayerOnline(@NotNull Player player) {
setPlayerOnline(inventories, player);
setPlayerOnline(enderChests, player);
if (player.hasPlayedBefore()) {
return;
}
// New player may have a name that already points to someone else in lookup cache.
String name = player.getName();
this.offlineLookUpCache.invalidate(name);
// If no offline matches are mapped, don't hit scheduler.
if (this.offlineLookUpCache.size() == 0) {
return;
}
// New player may also be a more exact match than one already in the cache.
// I.e. new player "lava1" is a better match for "lava" than "lava123"
// Player joins are already quite intensive, so this is run on a delay.
this.getServer().getScheduler().runTaskLaterAsynchronously(this, () -> {
Iterator<Map.Entry<String, PlayerProfile>> iterator = this.offlineLookUpCache.asMap().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, PlayerProfile> 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();
}
}
}, 101L); // Odd delay for pseudo load balancing; Player tasks are usually scheduled with full seconds.
}
private void setPlayerOnline(
@NotNull Map<UUID, ? extends ISpecialInventory> map,
@NotNull Player player) {
ISpecialInventory inventory = map.get(player.getUniqueId());
if (inventory == null) {
// Inventory not open.
return;
}
inventory.setPlayerOnline(player);
// Eject viewers lacking permission.
ejectViewers(
inventory,
viewer ->
!Permissions.ACCESS_ONLINE.hasPermission(viewer)
|| (!Permissions.ACCESS_CROSSWORLD.hasPermission(viewer)
&& !Objects.equals(viewer.getWorld(), inventory.getPlayer().getWorld())));
}
static void ejectViewers(@NotNull ISpecialInventory inventory, @NotNull Predicate<@NotNull HumanEntity> predicate) {
Inventory bukkitInventory = inventory.getBukkitInventory();
for (HumanEntity viewer : new ArrayList<>(bukkitInventory.getViewers())) {
if (viewer.getUniqueId().equals(inventory.getPlayer().getUniqueId())
&& !viewer.getOpenInventory().getTopInventory().equals(bukkitInventory)) {
// Skip owner with other inventory open. They aren't actually a viewer.
continue;
}
if (predicate.test(viewer)) {
viewer.closeInventory();
}
}
inventoryManager.unload(offline.getUniqueId());
}
}
@@ -1,102 +0,0 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv;
import com.google.errorprone.annotations.Keep;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.lang.LanguageManager;
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;
import org.jetbrains.annotations.NotNull;
class PlayerListener implements Listener {
private final @NotNull OpenInv plugin;
private final @NotNull LanguageManager lang;
PlayerListener(@NotNull OpenInv plugin, @NotNull LanguageManager lang) {
this.plugin = plugin;
this.lang = lang;
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
plugin.setPlayerOnline(event.getPlayer());
}
@Keep
@EventHandler(priority = EventPriority.MONITOR)
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
plugin.setPlayerOffline(event.getPlayer());
}
@Keep
@EventHandler
private void onWorldChange(@NotNull PlayerChangedWorldEvent event) {
plugin.changeWorld(event.getPlayer());
}
@Keep
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerInteract(@NotNull 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.CONTAINER_ANY.hasPermission(player) && plugin.getAnyContainerStatus(player);
boolean needsAny = plugin.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
if (!any && needsAny) {
return;
}
boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && plugin.getSilentContainerStatus(player);
// If anycontainer or silentcontainer is active
if (any || silent) {
if (plugin.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);
}
}
}
@@ -14,32 +14,31 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
package com.lishid.openinv.command;
import com.lishid.openinv.IOpenInv;
import com.lishid.openinv.event.OpenEvents;
import com.lishid.openinv.util.setting.PlayerToggle;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.setting.PlayerToggles;
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.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.function.BiConsumer;
import java.util.function.Predicate;
import java.util.UUID;
public class ContainerSettingCommand implements TabExecutor {
private final @NotNull IOpenInv plugin;
private final @NotNull LanguageManager lang;
public ContainerSettingCommand(@NotNull IOpenInv plugin, @NotNull LanguageManager lang) {
this.plugin = plugin;
public ContainerSettingCommand(@NotNull LanguageManager lang) {
this.lang = lang;
}
@@ -50,40 +49,52 @@ public class ContainerSettingCommand implements TabExecutor {
return true;
}
boolean any = command.getName().startsWith("any");
Predicate<Player> getSetting = any ? plugin::getAnyContainerStatus : plugin::getSilentContainerStatus;
BiConsumer<OfflinePlayer, Boolean> setSetting = any ? plugin::setAnyContainerStatus : plugin::setSilentContainerStatus;
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")) {
setSetting.accept(player, true);
set(toggle, playerId, true);
} else if (args[0].equals("off")) {
setSetting.accept(player, false);
set(toggle, playerId, false);
} else if (!args[0].equals("check")) {
// Invalid argument, show usage.
return false;
}
} else {
setSetting.accept(player, !getSetting.test(player));
set(toggle, playerId, !toggle.is(playerId));
}
String onOff = lang.getLocalizedMessage(player, getSetting.test(player) ? "messages.info.on" : "messages.info.off");
String onOff = lang.getLocalizedMessage(player, toggle.is(playerId) ? "messages.info.on" : "messages.info.off");
if (onOff == null) {
onOff = String.valueOf(getSetting.test(player));
onOff = String.valueOf(toggle.is(playerId));
}
lang.sendMessage(
sender,
"messages.info.settingState",
new Replacement("%setting%", any ? "AnyContainer" : "SilentContainer"),
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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.testPermissionSilent(sender) || args.length != 1) {
@@ -14,12 +14,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
package com.lishid.openinv.command;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.util.AccessEqualMode;
import com.lishid.openinv.util.InventoryManager;
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 org.bukkit.OfflinePlayer;
@@ -40,13 +44,24 @@ import java.util.logging.Level;
public class OpenInvCommand implements TabExecutor {
private final @NotNull OpenInv plugin;
private final @NotNull Config config;
private final @NotNull InventoryManager manager;
private final @NotNull LanguageManager lang;
private final @NotNull PlayerLoader playerLoader;
private final HashMap<Player, String> openInvHistory = new HashMap<>();
private final HashMap<Player, String> openEnderHistory = new HashMap<>();
public OpenInvCommand(@NotNull OpenInv plugin, @NotNull LanguageManager lang) {
public OpenInvCommand(
@NotNull OpenInv plugin,
@NotNull Config config,
@NotNull InventoryManager manager,
@NotNull LanguageManager lang,
@NotNull PlayerLoader playerLoader) {
this.plugin = plugin;
this.config = config;
this.manager = manager;
this.lang = lang;
this.playerLoader = playerLoader;
}
@Override
@@ -64,7 +79,7 @@ public class OpenInvCommand implements TabExecutor {
}
String noArgValue;
if (plugin.noArgsOpensSelf()) {
if (config.doesNoArgsOpenSelf()) {
noArgValue = player.getUniqueId().toString();
} else {
// History management
@@ -87,7 +102,7 @@ public class OpenInvCommand implements TabExecutor {
new BukkitRunnable() {
@Override
public void run() {
final OfflinePlayer offlinePlayer = OpenInvCommand.this.plugin.matchPlayer(name);
final OfflinePlayer offlinePlayer = playerLoader.match(name);
if (offlinePlayer == null || (!offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline())) {
lang.sendMessage(player, "messages.error.invalidPlayer");
@@ -143,9 +158,9 @@ public class OpenInvCommand implements TabExecutor {
boolean online = target.isOnline();
if (!online) {
if (!plugin.disableOfflineAccess() && Permissions.ACCESS_OFFLINE.hasPermission(player)) {
if (!config.isOfflineDisabled() && Permissions.ACCESS_OFFLINE.hasPermission(player)) {
// Try loading the player's data
onlineTarget = this.plugin.loadPlayer(target);
onlineTarget = playerLoader.load(target);
} else {
lang.sendMessage(player, "messages.error.permissionPlayerOffline");
return;
@@ -183,7 +198,7 @@ public class OpenInvCommand implements TabExecutor {
for (int level = 4; level > 0; --level) {
String permission = "openinv.access.level." + level;
if (onlineTarget.hasPermission(permission)
&& !player.hasPermission(permission)) {
&& (!player.hasPermission(permission) || config.getAccessEqualMode() == AccessEqualMode.DENY)) {
lang.sendMessage(
player,
"messages.error.permissionExempt",
@@ -203,7 +218,7 @@ public class OpenInvCommand implements TabExecutor {
}
}
if (!plugin.noArgsOpensSelf()) {
if (!config.doesNoArgsOpenSelf()) {
// Record the target
(openinv ? this.openInvHistory : this.openEnderHistory).put(player, target.getUniqueId().toString());
}
@@ -211,7 +226,7 @@ public class OpenInvCommand implements TabExecutor {
// Create the inventory
final ISpecialInventory inv;
try {
inv = openinv ? this.plugin.getSpecialInventory(onlineTarget, online) : this.plugin.getSpecialEnderChest(onlineTarget, online);
inv = openinv ? manager.getInventory(onlineTarget) : manager.getEnderChest(onlineTarget);
} catch (Exception e) {
lang.sendMessage(player, "messages.error.commandException");
plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e);
@@ -14,7 +14,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
package com.lishid.openinv.command;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
@@ -14,7 +14,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
package com.lishid.openinv.command;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
@@ -46,9 +46,9 @@ import java.util.Locale;
*/
public class SearchEnchantCommand implements TabExecutor {
private final LanguageManager lang;
private final @NotNull LanguageManager lang;
public SearchEnchantCommand(LanguageManager lang) {
public SearchEnchantCommand(@NotNull LanguageManager lang) {
this.lang = lang;
}
@@ -14,7 +14,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
package com.lishid.openinv.command;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
@@ -34,9 +34,9 @@ import java.util.List;
public class SearchInvCommand implements TabExecutor {
private final LanguageManager lang;
private final @NotNull LanguageManager lang;
public SearchInvCommand(LanguageManager lang) {
public SearchInvCommand(@NotNull LanguageManager lang) {
this.lang = lang;
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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.setting.PlayerToggles;
import com.lishid.openinv.util.lang.LanguageManager;
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.hasPermission(player) && PlayerToggles.any().is(playerId);
boolean needsAny = accessor.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
if (!any && needsAny) {
return;
}
boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && 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);
}
}
}
@@ -1,12 +1,13 @@
package com.lishid.openinv;
package com.lishid.openinv.listener;
import com.google.errorprone.annotations.Keep;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.util.AccessEqualMode;
import com.lishid.openinv.util.InventoryAccess;
import com.lishid.openinv.util.Permissions;
import org.bukkit.GameMode;
import com.lishid.openinv.util.config.Config;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
@@ -14,50 +15,34 @@ 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.InventoryHolder;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
record LegacyInventoryListener(OpenInv plugin) implements Listener {
/**
* A listener used to enable functionality and prevent issues on versions < 1.21.
*/
public class LegacyInventoryListener implements Listener {
@Keep
@EventHandler
private void onInventoryClose(@NotNull final InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player player)) {
return;
}
private final @NotNull Plugin plugin;
private final @NotNull Config config;
InventoryHolder holder = event.getInventory().getHolder();
if (this.plugin.getSilentContainerStatus(player)
&& holder != null
&& this.plugin.getAnySilentContainer().isAnySilentContainer(holder)) {
this.plugin.getAnySilentContainer().deactivateContainer(player);
}
ISpecialInventory specialInventory = InventoryAccess.getEnderChest(event.getInventory());
if (specialInventory != null) {
this.plugin.handleCloseInventory(specialInventory);
} else {
specialInventory = InventoryAccess.getPlayerInventory(event.getInventory());
if (specialInventory != null) {
this.plugin.handleCloseInventory(specialInventory);
}
}
public LegacyInventoryListener(@NotNull Plugin plugin, @NotNull Config config) {
this.plugin = plugin;
this.config = config;
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
private void onInventoryClick(@NotNull final InventoryClickEvent event) {
if (handleInventoryInteract(event)) {
return;
@@ -86,13 +71,18 @@ record LegacyInventoryListener(OpenInv plugin) implements Listener {
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
private void onInventoryDrag(@NotNull final InventoryDragEvent event) {
if (handleInventoryInteract(event)) {
return;
}
InventoryView view = event.getView();
if (view.getCursor() == null) {
return;
}
int topSize = view.getTopInventory().getSize();
// Get bottom inventory active slots as player inventory slots.
@@ -123,15 +113,29 @@ record LegacyInventoryListener(OpenInv plugin) implements Listener {
return;
}
ItemStack cursor = event.getCursor();
if (cursor != null) {
cursor.setAmount(cursor.getAmount() + overlapLosses);
} else {
cursor = event.getOldCursor().clone();
cursor.setAmount(overlapLosses);
}
final ItemStack lost = view.getCursor().clone();
lost.setAmount(overlapLosses);
event.setCursor(cursor);
// Re-add the lost items in the same tick after the event has completed.
plugin.getServer().getScheduler().runTask(plugin, () -> {
InventoryView currentOpen = event.getWhoClicked().getOpenInventory();
if (!currentOpen.equals(view)) {
event.getWhoClicked().getWorld().dropItem(event.getWhoClicked().getLocation(), lost).setPickupDelay(0);
return;
}
ItemStack cursor = currentOpen.getCursor();
if (cursor == null) {
currentOpen.setCursor(lost);
} else if (lost.isSimilar(cursor)) {
cursor.setAmount(cursor.getAmount() + lost.getAmount());
currentOpen.setCursor(cursor);
} else {
event.getWhoClicked().getWorld().dropItem(event.getWhoClicked().getLocation(), lost).setPickupDelay(0);
}
});
}
private int getCountDiff(@Nullable ItemStack original, @NotNull ItemStack result) {
@@ -149,44 +153,55 @@ record LegacyInventoryListener(OpenInv plugin) implements Listener {
* @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_CLICK.hasPermission(entity) && entity.getGameMode() == GameMode.SPECTATOR) {
event.setCancelled(false);
}
if (event.isCancelled()) {
return true;
}
HumanEntity viewer = event.getWhoClicked();
Inventory inventory = event.getView().getTopInventory();
ISpecialInventory backing = InventoryAccess.getInventory(inventory);
Permissions editSelf;
Permissions editOther;
if (backing instanceof ISpecialEnderChest) {
editSelf = Permissions.ENDERCHEST_EDIT_SELF;
editOther = Permissions.ENDERCHEST_EDIT_OTHER;
} else if (backing instanceof ISpecialPlayerInventory) {
editSelf = Permissions.INVENTORY_EDIT_SELF;
editOther = Permissions.INVENTORY_EDIT_OTHER;
if (backing == null) {
return true;
}
Permissions edit;
HumanEntity target = backing.getPlayer();
boolean ownContainer = viewer.equals(target);
if (backing instanceof ISpecialPlayerInventory) {
edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER;
} else if (backing instanceof ISpecialEnderChest) {
edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_OPEN_OTHER;
} else {
// Unknown implementation.
return true;
}
if (Objects.equals(entity, backing.getPlayer())) {
if (!editSelf.hasPermission(entity)) {
event.setCancelled(true);
return true;
}
return !(backing instanceof ISpecialPlayerInventory);
} else {
if (!editOther.hasPermission(entity)) {
event.setCancelled(true);
}
if (!edit.hasPermission(viewer)) {
event.setCancelled(true);
return true;
}
// If access ties aren't view-only mode, don't bother with permission checks.
if (config.getAccessEqualMode() != AccessEqualMode.VIEW) {
return !ownContainer || !(backing instanceof ISpecialPlayerInventory);
}
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 (viewer.hasPermission(permission)) {
break;
}
// Otherwise check next access level.
continue;
}
// Either the viewer lacks access (which shouldn't be possible) or this is a tie. View-only.
event.setCancelled(true);
return true;
}
return !ownContainer || !(backing instanceof ISpecialPlayerInventory);
}
private static int convertToPlayerSlot(InventoryView view, int rawSlot) {
@@ -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);
}
}
@@ -0,0 +1,23 @@
package com.lishid.openinv.util;
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;
};
}
}
@@ -1,162 +0,0 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.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 record ConfigUpdater(OpenInv 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();
}
if (version < 5) {
updateConfig4To5();
}
if (version < 6) {
updateConfig5To6();
}
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.saveConfig();
plugin.getLogger().info("Configuration update complete!");
});
});
}
private void updateConfig5To6() {
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("settings.command.open.no-args-opens-self", false);
plugin.getConfig().set("settings.command.searchcontainer.max-radius", 10);
plugin.getConfig().set("config-version", 6);
});
}
private void updateConfig4To5() {
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("settings.disable-offline-access", false);
plugin.getConfig().set("config-version", 5);
});
}
private void updateConfig3To4() {
plugin.getServer().getScheduler().runTask(plugin, () -> {
plugin.getConfig().set("notify", null);
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<String> keys = section.getKeys(false);
// Ensure section has content
if (keys.isEmpty()) {
return;
}
final Map<String, Boolean> toggles = new HashMap<>();
for (String playerName : keys) {
OfflinePlayer player = plugin.matchPlayer(playerName);
if (player != null) {
toggles.put(player.getUniqueId().toString(), 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<String, Boolean> entry : toggles.entrySet()) {
newSection.set(entry.getKey(), entry.getValue());
}
});
}
}
@@ -21,11 +21,13 @@ 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.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
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.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -121,32 +123,6 @@ public class InternalAccessor {
return "https://github.com/Jikoo/OpenInv/releases";
}
/**
* Creates an 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();
}
/**
* Creates an instance of the IPlayerDataManager implementation for the current server version.
*
* @return the IPlayerDataManager
* @throws IllegalStateException if server version is unsupported
*/
public @NotNull PlayerManager getPlayerDataManager() {
if (internal == null) {
throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
}
return internal.getPlayerManager();
}
/**
* Reload internal features.
*/
@@ -174,13 +150,46 @@ public class InternalAccessor {
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
*/
public ISpecialEnderChest newSpecialEnderChest(final Player player) {
ISpecialEnderChest createEnderChest(final Player player) {
if (internal == null) {
throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
}
@@ -193,7 +202,7 @@ public class InternalAccessor {
* @param player the Player
* @return the ISpecialPlayerInventory created
*/
public ISpecialPlayerInventory newSpecialPlayerInventory(final Player player) {
ISpecialPlayerInventory createInventory(final Player player) {
if (internal == null) {
throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT));
}
@@ -0,0 +1,274 @@
package com.lishid.openinv.util;
import com.github.jikoo.planarwrappers.util.version.BukkitVersions;
import com.github.jikoo.planarwrappers.util.version.Version;
import com.google.errorprone.annotations.Keep;
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.bukkit.inventory.Inventory;
import org.bukkit.plugin.Plugin;
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<UUID, ISpecialPlayerInventory> inventories = new ConcurrentHashMap<>();
private final Map<UUID, ISpecialEnderChest> enderChests = new ConcurrentHashMap<>();
private final Set<UUID> expectedCloses = new HashSet<>();
private final @NotNull Plugin plugin;
private final @NotNull Config config;
private final @NotNull InternalAccessor accessor;
public InventoryManager(@NotNull Plugin 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);
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
consumeLoaded(event.getPlayer().getUniqueId(), inventory -> 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<UUID, ? extends ISpecialInventory> 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.getServer().getScheduler().runTask(plugin, () -> {
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<UUID, ? extends ISpecialInventory> 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 <T extends ISpecialInventory> 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<HumanEntity> viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers());
// Legacy: Owner is always a viewer of own inventory.
if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 21))) {
if (inventory instanceof ISpecialPlayerInventory) {
Inventory active = owner.getOpenInventory().getTopInventory();
if (!active.equals(inventory.getBukkitInventory())) {
viewers.remove(owner);
}
}
}
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 <T extends ISpecialInventory> boolean consumeLoaded(
@NotNull Map<UUID, T> 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 <T extends ISpecialInventory> @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;
}
}
@@ -0,0 +1,222 @@
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.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.plugin.Plugin;
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.ExecutionException;
import java.util.concurrent.Future;
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 Plugin 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<String, PlayerProfile> lookupCache;
public PlayerLoader(
@NotNull Plugin 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);
}
Future<Player> future = Bukkit.getScheduler().callSyncMethod(plugin,
() -> 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 match(@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;
}
// 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.getServer().getScheduler().runTaskLaterAsynchronously(plugin, () -> {
Iterator<Map.Entry<String, PlayerProfile>> iterator = lookupCache.asMap().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, PlayerProfile> 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);
}
}
@@ -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;
}
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
@@ -1,4 +1,4 @@
package com.lishid.openinv.util;
package com.lishid.openinv.util.lang;
import org.jetbrains.annotations.NotNull;
+3 -2
View File
@@ -1,5 +1,6 @@
config-version: 6
config-version: 8
settings:
equal-access: view
command:
open:
no-args-opens-self: false
@@ -7,4 +8,4 @@ settings:
max-radius: 10
disable-offline-access: false
disable-saving: false
locale: 'en_us'
console-locale: 'en'
+4 -3
View File
@@ -24,7 +24,7 @@
<modules>
<module>common</module>
</modules>
<version>5.0.0</version>
<version>5.1.0</version>
<packaging>pom</packaging>
@@ -41,6 +41,7 @@
<id>all</id>
<modules>
<module>api</module>
<module>addon/togglepersist</module>
<module>common</module>
<module>internal/v1_21_R1</module>
<module>internal/v1_20_R4</module>
@@ -134,7 +135,7 @@
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>3.4.1</version>
<version>3.4.2</version>
<configuration>
<archive>
<manifestEntries>
@@ -151,7 +152,7 @@
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.28.0</version>
<version>2.29.2</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
@@ -1,7 +1,7 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "minecraft:item/white_stained_glass_pane"
"layer0": "minecraft:block/white_stained_glass"
},
"overrides": [
{